Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions handlebars-jackson3/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<parent>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars.java</artifactId>
<version>4.5.4</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>handlebars-jackson3</artifactId>

<name>JSON Jackson helpers</name>

<dependencies>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<classifier>tests</classifier>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* Handlebars.java: https://github.com/jknack/handlebars.java
* Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
* Copyright (c) 2012 Edgar Espina
*/
package com.github.jknack.handlebars.jackson;

import com.github.jknack.handlebars.Handlebars;
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;
import tools.jackson.core.SerializableString;
import tools.jackson.core.io.CharacterEscapes;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectWriter;
import tools.jackson.databind.json.JsonMapper;

import java.util.HashMap;
import java.util.Map;

import static java.util.Objects.requireNonNull;

/**
* A Jackson 3.x helper.
*
* <p>Basic usage:
*
* <pre>
* Handlebars hbs = new Handlebars();
*
* hbs.registerHelper("json", JacksonHelper.INSTANCE);
*
* ...
*
* {{json model}}
* </pre>
*
* <p>If <code>model</code> is null an empty string is returned.
*
* <p>You can change this using the <code>default</code> option:
*
* <pre>
* {{json model default="{}"}}
* </pre>
*
* <p>Using a view class:
*
* <pre>
* {{json model view="foo.MyView"}}
* </pre>
*
* <p>Using alias for views:
*
* <pre>
* {{json model view="myView"}}
* </pre>
*
* <p>Escape HTML chars:
*
* <pre>
* {{json model escapeHTML=true}}
* </pre>
*
* <p>Pretty printer:
*
* <pre>
* {{json model pretty=true}}
* </pre>
*
* @author edgar.espina, jolarsen
* @since 4.5.5
*/
public class JacksonHelper implements Helper<Object> {

/**
* Escape HTML chars from JSON content. See
* http://www.cowtowncoder.com/blog/archives/2012/08/entry_476.html
*
* @author edgar.espina, jolarsen
* @since 4.5.5
*/
@SuppressWarnings("serial")
private static class HtmlEscapes extends CharacterEscapes {

/** The escape table. */
private final int[] escapeTable;

{
// Start with set of characters known to require escaping (double-quote,
// backslash etc)
escapeTable = CharacterEscapes.standardAsciiEscapesForJSON();
// and force escaping of a few others:
escapeTable['<'] = CharacterEscapes.ESCAPE_STANDARD;
escapeTable['>'] = CharacterEscapes.ESCAPE_STANDARD;
escapeTable['&'] = CharacterEscapes.ESCAPE_STANDARD;
escapeTable['\''] = CharacterEscapes.ESCAPE_STANDARD;
}

@Override
public int[] getEscapeCodesForAscii() {
return escapeTable;
}

@Override
public SerializableString getEscapeSequence(final int ch) {
return null;
}
}

/** A singleton version of {@link JacksonHelper}. */
public static final Helper<Object> INSTANCE = new JacksonHelper();

/** The JSON parser. */
private final ObjectMapper mapper;

/** Class alias registry. */
private final Map<String, Class<?>> alias = new HashMap<String, Class<?>>();

/**
* Creates a new {@link JacksonHelper}.
*
* @param objectMapper The object's mapper. Required.
*/
public JacksonHelper(final ObjectMapper objectMapper) {
mapper = requireNonNull(objectMapper, "The object mapper is required.");
}

/** Creates a new {@link JacksonHelper}. */
private JacksonHelper() {
this(JsonMapper.builder().build());
}

@Override
public Object apply(final Object context, final Options options) {
if (context == null) {
return options.hash("default", "");
}

final String viewName = options.hash("view", "");
try {
final Class<?> viewClass = alias.get(viewName);
final ObjectWriter viewWriter = Handlebars.Utils.isEmpty(viewName)
? mapper.writer()
: mapper.writerWithView(viewClass != null
? viewClass
: getClass().getClassLoader().loadClass(viewName));

final boolean escapeHtml = options.hash("escapeHTML", false);
final boolean pretty = options.hash("pretty", false);

final ObjectWriter writer = pretty
? viewWriter.withDefaultPrettyPrinter()
: viewWriter;

final ObjectWriter configuredWriter = escapeHtml
? writer.with(new HtmlEscapes())
: writer;

return new Handlebars.SafeString(configuredWriter.writeValueAsString(context));
} catch (ClassNotFoundException|NoClassDefFoundError ex) {
throw new IllegalArgumentException(viewName, ex);
}
}

/**
* Add an alias for the given view class.
*
* @param alias The view alias. Required.
* @param viewClass The view class. Required.
* @return This helper.
*/
public JacksonHelper viewAlias(final String alias, final Class<?> viewClass) {
this.alias.put(
requireNonNull(alias, "A view alias is required."),
requireNonNull(viewClass, "A view class is required."));
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Handlebars.java: https://github.com/jknack/handlebars.java
* Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
* Copyright (c) 2012 Edgar Espina
*/
package com.github.jknack.handlebars.jackson;

import com.github.jknack.handlebars.ValueResolver;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.BigIntegerNode;
import tools.jackson.databind.node.BinaryNode;
import tools.jackson.databind.node.BooleanNode;
import tools.jackson.databind.node.DecimalNode;
import tools.jackson.databind.node.DoubleNode;
import tools.jackson.databind.node.IntNode;
import tools.jackson.databind.node.LongNode;
import tools.jackson.databind.node.NullNode;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.node.POJONode;
import tools.jackson.databind.node.StringNode;

import java.util.AbstractMap;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
* Resolve a context stack value from {@link JsonNode}.
*
* @author edgar.espina, jolarsen
* @since 4.5.5
*/
public enum JsonNodeValueResolver implements ValueResolver {

/** The singleton instance. */
INSTANCE;

@Override
public Object resolve(final Object context, final String name) {
Object value = null;
if (context instanceof ArrayNode arrayNode) {
try {
if (name.equals("length")) {
return arrayNode.size();
}
value = resolve(arrayNode.get(Integer.parseInt(name)));
} catch (NumberFormatException ex) {
// ignore undefined key and move on, see
// https://github.com/jknack/handlebars.java/pull/280
}
} else if (context instanceof JsonNode jsonNode) {
value = resolve(jsonNode.get(name));
}
return value == null ? UNRESOLVED : value;
}

@Override
public Object resolve(final Object context) {
if (context instanceof JsonNode jsonNode) {
return resolve(jsonNode);
}
return UNRESOLVED;
}

/**
* Resolve a {@link JsonNode} object to a primitive value.
*
* @param node A {@link JsonNode} object.
* @return A primitive value, json object, json array or null.
*/
private static Object resolve(final JsonNode node) {
// binary node
if (node instanceof BinaryNode binaryNode) {
return binaryNode.binaryValue();
}
// boolean node
if (node instanceof BooleanNode booleanNode) {
return booleanNode.booleanValue();
}
// null node
if (node instanceof NullNode) {
return null;
}
// numeric nodes
if (node instanceof BigIntegerNode bigIntegerNode) {
return bigIntegerNode.bigIntegerValue();
}
if (node instanceof DecimalNode decimalNode) {
return decimalNode.decimalValue();
}
if (node instanceof DoubleNode doubleNode) {
return doubleNode.doubleValue();
}
if (node instanceof IntNode intNode) {
return intNode.intValue();
}
if (node instanceof LongNode longNode) {
return longNode.longValue();
}
// pojo
if (node instanceof POJONode pojoNode) {
return pojoNode.getPojo();
}
// string
if (node instanceof StringNode stringNode) {
return stringNode.stringValue();
}
// object node to map
if (node instanceof ObjectNode objectNode) {
return toMap(objectNode);
}
// container, array or null
return node;
}

/**
* @param node A json node.
* @return A map from a json node.
*/
private static Map<String, Object> toMap(final ObjectNode node) {
return new AbstractMap<String, Object>() {
@Override
public Object get(final Object key) {
return resolve(node.get((String) key));
}

@Override
public int size() {
return node.size();
}

@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Set<Entry<String, Object>> entrySet() {
Set set = new LinkedHashSet<Map.Entry<String, Object>>();
node.propertyStream().forEach(set::add);
return set;
}
};
}

@Override
public Set<Entry<String, Object>> propertySet(final Object context) {
if (context instanceof ObjectNode node) {
Map<String, Object> result = new LinkedHashMap<>();
node.propertyNames().forEach(name -> result.put(name, resolve(node.get(name))));
return result.entrySet();
}
return Collections.emptySet();
}
}
Loading
Loading