Skip to content

Commit

Permalink
#20 Pebble Engine options. Unit tests for different settings and syntax.
Browse files Browse the repository at this point in the history
  • Loading branch information
marcinus committed Jan 24, 2020
1 parent a22ff77 commit 389f6c0
Show file tree
Hide file tree
Showing 24 changed files with 538 additions and 130 deletions.
2 changes: 1 addition & 1 deletion pebble/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Templates. It's key is computed basing on the `cacheKeyAlgorithm` defined in the
(the default is `MD5` of the Fragment's `body`).

## How to configure
For all configuration fields and their defaults consult [io.knotx.te.pebble.PebbleEngineOptions](https://github.com/Knotx/knotx-template-engine/blob/master/handlebars/docs/asciidoc/dataobjects.adoc)
For all configuration fields and their defaults consult [io.knotx.te.pebble.options.PebbleEngineOptions](https://github.com/Knotx/knotx-template-engine/blob/master/handlebars/docs/asciidoc/dataobjects.adoc)

### Interpolation symbol
By default, the Handlebars engine uses `{{` and `}}` symbols as tag delimiters.
Expand Down
2 changes: 1 addition & 1 deletion pebble/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies {
implementation(group = "io.vertx", name = "vertx-service-proxy")
implementation(group = "io.vertx", name = "vertx-rx-java2")
implementation(group = "com.google.guava", name = "guava")
implementation(group = "io.pebbletemplates", name= "pebble", version = "3.0.0")
implementation(group = "io.pebbletemplates", name= "pebble", version = "3.1.2")

testImplementation("io.knotx:knotx-junit5:${project.version}")
testImplementation(group = "org.mockito", name = "mockito-core")
Expand Down
33 changes: 24 additions & 9 deletions pebble/docs/asciidoc/dataobjects.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
== PebbleEngineOptions

++++
Describes Peeble Knot configuration
Describes Pebble Knot configuration
++++
'''

Expand All @@ -22,13 +22,28 @@ Sets the algorithm used to build a hash from the handlebars snippet. The hash is
Sets the size of the cache. After reaching the max size, new elements will replace the oldest
one.
+++
|[[endDelimiter]]`@endDelimiter`|`String`|+++
Sets the end delimiter for the Handlebars engine to recognize en of placeholders. By default,
the Handlebars engine uses `}}` symbols as end delimiter.
+++
|[[startDelimiter]]`@startDelimiter`|`String`|+++
Sets the start delimiter for the Handlebars engine to recognize start of placeholders. By
default, the Handlebars engine uses `{{` symbols as start delimiter.
+++
|[[literalDecimalTreatedAsInteger]]`@literalDecimalTreatedAsInteger`|`Boolean`|-
|[[newLineTrimming]]`@newLineTrimming`|`Boolean`|-
|[[strictVariables]]`@strictVariables`|`Boolean`|-
|[[syntax]]`@syntax`|`link:dataobjects.html#PebbleEngineSyntaxOptions[PebbleEngineSyntaxOptions]`|-
|===

[[PebbleEngineSyntaxOptions]]
== PebbleEngineSyntaxOptions


[cols=">25%,25%,50%"]
[frame="topbot"]
|===
^|Name | Type ^| Description
|[[delimiterCommentClose]]`@delimiterCommentClose`|`String`|-
|[[delimiterCommentOpen]]`@delimiterCommentOpen`|`String`|-
|[[delimiterExecuteClose]]`@delimiterExecuteClose`|`String`|-
|[[delimiterExecuteOpen]]`@delimiterExecuteOpen`|`String`|-
|[[delimiterInterpolationClose]]`@delimiterInterpolationClose`|`String`|-
|[[delimiterInterpolationOpen]]`@delimiterInterpolationOpen`|`String`|-
|[[delimiterPrintClose]]`@delimiterPrintClose`|`String`|-
|[[delimiterPrintOpen]]`@delimiterPrintOpen`|`String`|-
|[[whitespaceTrim]]`@whitespaceTrim`|`String`|-
|===

94 changes: 62 additions & 32 deletions pebble/src/main/java/io/knotx/te/pebble/PebbleTemplateEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
import com.mitchellbosecke.pebble.template.PebbleTemplate;
import io.knotx.fragments.api.Fragment;
import io.knotx.te.api.TemplateEngine;
import io.knotx.te.pebble.options.PebbleEngineOptions;
import io.knotx.te.pebble.options.PebbleEngineSyntaxComposer;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.reactivex.core.Vertx;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
Expand All @@ -40,47 +41,25 @@ class PebbleTemplateEngine implements TemplateEngine {
private final Cache<String, PebbleTemplate> cache;
private final MessageDigest digest;

PebbleTemplateEngine(Vertx vertx, PebbleEngineOptions options) {
PebbleTemplateEngine(PebbleEngineOptions options) {
LOGGER.info("<{}> instance created", this.getClass().getSimpleName());
this.pebbleEngine = new PebbleEngine.Builder().loader(new StringLoader()).cacheActive(false)
.build();
this.cache = CacheBuilder.newBuilder()
.maximumSize(options.getCacheSize())
.removalListener(listener -> LOGGER.warn(
"Cache limit exceeded. Revisit 'cacheSize' setting"))
.build();
try {
this.digest = MessageDigest.getInstance(options.getCacheKeyAlgorithm());
} catch (NoSuchAlgorithmException e) {
LOGGER.error("No such algorithm available {}.", options.getCacheKeyAlgorithm(), e);
throw new IllegalArgumentException(e);
}
this.pebbleEngine = createPebbleEngine(options);
this.cache = createCache(options);
this.digest = tryToCreateDigest(options);
}

@Override
public String process(Fragment fragment) {
PebbleTemplate template = template(fragment);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Processing with Peeble: {}!", fragment);
}
try {
StringWriter writer = new StringWriter();
template.evaluate(writer, fragment.getPayload().getMap());
return writer.toString();
} catch (IOException e) {
LOGGER.error("Could not apply context to fragment [{}]", fragment.abbreviate(), e);
throw new IllegalStateException(e);
}
PebbleTemplate template = getTemplate(fragment);
traceProcessingFragment(fragment);
return tryToProcessOnEngine(template, fragment);
}

private PebbleTemplate template(Fragment fragment) {
private PebbleTemplate getTemplate(Fragment fragment) {
try {
String cacheKey = getCacheKey(fragment);

return cache.get(cacheKey, () -> {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Compiled Pebble fragment [{}]", fragment);
}
traceCompilingFragment(fragment);
return pebbleEngine.getTemplate(fragment.getBody());
});
} catch (ExecutionException e) {
Expand All @@ -89,9 +68,60 @@ private PebbleTemplate template(Fragment fragment) {
}
}

private String tryToProcessOnEngine(PebbleTemplate template, Fragment fragment) {
try {
StringWriter writer = new StringWriter();
template.evaluate(writer, fragment.getPayload().getMap());
return writer.toString();
} catch (IOException e) {
LOGGER.error("Could not apply context to fragment [{}]", fragment.abbreviate(), e);
throw new IllegalStateException(e);
}
}

private String getCacheKey(Fragment fragment) {
byte[] cacheKeyBytes = digest.digest(fragment.getBody().getBytes(StandardCharsets.UTF_8));
return new String(cacheKeyBytes);
}

private PebbleEngine createPebbleEngine(PebbleEngineOptions options) {
return new PebbleEngine.Builder()
.loader(new StringLoader())
.cacheActive(false)
.strictVariables(options.isStrictVariables())
.newLineTrimming(options.isNewLineTrimming())
.syntax(PebbleEngineSyntaxComposer.compose(options.getSyntax()))
.literalDecimalTreatedAsInteger(options.isLiteralDecimalTreatedAsInteger())
.build();
}

private Cache<String, PebbleTemplate> createCache(PebbleEngineOptions options) {
return CacheBuilder.newBuilder()
.maximumSize(options.getCacheSize())
.removalListener(listener -> LOGGER.warn(
"Cache limit exceeded. Revisit 'cacheSize' setting"))
.build();
}

private MessageDigest tryToCreateDigest(PebbleEngineOptions options) {
try {
return MessageDigest.getInstance(options.getCacheKeyAlgorithm());
} catch (NoSuchAlgorithmException e) {
LOGGER.error("No such algorithm available {}.", options.getCacheKeyAlgorithm(), e);
throw new IllegalArgumentException(e);
}
}

private static void traceProcessingFragment(Fragment fragment) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Processing with Pebble: {}!", fragment);
}
}

private static void traceCompilingFragment(Fragment fragment) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Compiled Pebble fragment [{}]", fragment);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
package io.knotx.te.pebble;
/*
* Copyright (C) 2019 Knot.x Project
*
Expand All @@ -14,21 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.knotx.te.pebble;

import io.knotx.te.api.TemplateEngine;
import io.knotx.te.api.TemplateEngineFactory;
import io.knotx.te.pebble.options.PebbleEngineOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.core.Vertx;

public class PebbleTemplateEngineFactory implements TemplateEngineFactory {

@Override
public String getName() {
return "handlebars";
return "pebble";
}

@Override
public TemplateEngine create(Vertx vertx, JsonObject config) {
return new PebbleTemplateEngine(vertx, new PebbleEngineOptions(config));
return new PebbleTemplateEngine(new PebbleEngineOptions(config));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.knotx.te.pebble;/*
/*
* Copyright (C) 2019 Knot.x Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -13,38 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.knotx.te.pebble.options;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;

/**
* Describes Peeble Knot configuration
* Describes Pebble Knot configuration
*/
@DataObject(generateConverter = true, publicConverter = false)
public class PebbleEngineOptions {

private static final String DEFAULT_CACHE_KEY_ALGORITHM = "MD5";

private static final String DEFAULT_START_DELIMITER = "{{";

private static final String DEFAULT_END_DELIMITER = "}}";
private static final boolean DEFAULT_STRICT_VARIABLES = false;
private static final boolean DEFAULT_NEW_LINE_TRIMMING = true;
private static final boolean DEFAULT_LITERAL_DECIMAL_TREATED_AS_INTEGER = false;

private String cacheKeyAlgorithm;
private Long cacheSize;
private String startDelimiter;
private String endDelimiter;
private boolean strictVariables;
private boolean newLineTrimming;
private boolean literalDecimalTreatedAsInteger;
private PebbleEngineSyntaxOptions syntax;

public PebbleEngineOptions() {
init();
}

public PebbleEngineOptions(PebbleEngineOptions other) {
this.cacheKeyAlgorithm = other.cacheKeyAlgorithm;
this.cacheSize = other.cacheSize;
this.startDelimiter = other.startDelimiter;
this.endDelimiter = other.endDelimiter;
}

public PebbleEngineOptions(JsonObject json) {
init();
PebbleEngineOptionsConverter.fromJson(json, this);
Expand All @@ -58,8 +53,10 @@ public JsonObject toJson() {

private void init() {
cacheKeyAlgorithm = DEFAULT_CACHE_KEY_ALGORITHM;
startDelimiter = DEFAULT_START_DELIMITER;
endDelimiter = DEFAULT_END_DELIMITER;
strictVariables = DEFAULT_STRICT_VARIABLES;
newLineTrimming = DEFAULT_NEW_LINE_TRIMMING;
literalDecimalTreatedAsInteger = DEFAULT_LITERAL_DECIMAL_TREATED_AS_INTEGER;
syntax = new PebbleEngineSyntaxOptions();
}

/**
Expand Down Expand Up @@ -102,36 +99,36 @@ public PebbleEngineOptions setCacheKeyAlgorithm(String cacheKeyAlgorithm) {
return this;
}

public String getStartDelimiter() {
return startDelimiter;
public boolean isStrictVariables() {
return strictVariables;
}

/**
* Sets the start delimiter for the Handlebars engine to recognize start of placeholders. By
* default, the Handlebars engine uses `{{` symbols as start delimiter.
*
* @param startDelimiter - the delimiter that will distinguish beginning of the handlebars
* expression
* @return a reference to this, so the API can be used fluently
*/
public PebbleEngineOptions setStartDelimiter(String startDelimiter) {
this.startDelimiter = startDelimiter;
return this;
public void setStrictVariables(boolean strictVariables) {
this.strictVariables = strictVariables;
}

public String getEndDelimiter() {
return endDelimiter;
public PebbleEngineSyntaxOptions getSyntax() {
return syntax;
}

/**
* Sets the end delimiter for the Handlebars engine to recognize en of placeholders. By default,
* the Handlebars engine uses `}}` symbols as end delimiter.
*
* @param endDelimiter - the delimiter that will distinguish end of the handlebars expression
* @return a reference to this, so the API can be used fluently
*/
public PebbleEngineOptions setEndDelimiter(String endDelimiter) {
this.endDelimiter = endDelimiter;
return this;
public void setSyntax(
PebbleEngineSyntaxOptions syntax) {
this.syntax = syntax;
}

public boolean isNewLineTrimming() {
return newLineTrimming;
}

public void setNewLineTrimming(boolean newLineTrimming) {
this.newLineTrimming = newLineTrimming;
}

public boolean isLiteralDecimalTreatedAsInteger() {
return literalDecimalTreatedAsInteger;
}

public void setLiteralDecimalTreatedAsInteger(boolean literalDecimalTreatedAsInteger) {
this.literalDecimalTreatedAsInteger = literalDecimalTreatedAsInteger;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2019 Knot.x Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.knotx.te.pebble.options;

import com.mitchellbosecke.pebble.lexer.Syntax;

public final class PebbleEngineSyntaxComposer {

private PebbleEngineSyntaxComposer() {
// utility class
}

public static Syntax compose(PebbleEngineSyntaxOptions options) {
Syntax.Builder builder = new Syntax.Builder()
.setCommentOpenDelimiter(options.getDelimiterCommentOpen())
.setCommentCloseDelimiter(options.getDelimiterCommentClose())
.setExecuteOpenDelimiter(options.getDelimiterExecuteOpen())
.setExecuteCloseDelimiter(options.getDelimiterExecuteClose())
.setPrintOpenDelimiter(options.getDelimiterPrintOpen())
.setPrintCloseDelimiter(options.getDelimiterPrintClose())
.setWhitespaceTrim(options.getWhitespaceTrim());
builder.setInterpolationOpenDelimiter(options.getDelimiterInterpolationOpen());
builder.setInterpolationCloseDelimiter(options.getDelimiterInterpolationOpen());
return builder.build();
}

}
Loading

0 comments on commit 389f6c0

Please sign in to comment.