com.github.jknack.handlebars.Context Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of handlebars Show documentation
Show all versions of handlebars Show documentation
Logic-less and semantic templates with Java
The newest version!
/*
* 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;
import static org.apache.commons.lang3.Validate.notEmpty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.github.jknack.handlebars.context.MapValueResolver;
import com.github.jknack.handlebars.internal.path.ThisPath;
import com.github.jknack.handlebars.io.TemplateSource;
/**
* Mustache/Handlebars are contextual template engines. This class represent the 'context stack' of
* a template.
*
*
* - Objects and hashes should be pushed onto the context stack.
*
- All elements on the context stack should be accessible.
*
- Multiple sections per template should be permitted.
*
- Failed context lookups should be considered falsy.
*
- Dotted names should be valid for Section tags.
*
- Dotted names that cannot be resolved should be considered falsy.
*
- Dotted Names - Context Precedence: Dotted names should be resolved against former
* resolutions.
*
*
* @author edgar.espina
* @since 0.1.0
*/
public class Context {
/**
* Special scope for silly block param rules, implemented by handlebars.js. This context will
* check for pathed variable and resolve them against parent context.
*
* @author edgar
* @since 3.0.0
*/
private static class BlockParam extends Context {
/**
* A new {@link BlockParam}.
*
* @param parent Parent context.
* @param hash A hash model.
*/
protected BlockParam(final Context parent, final Map hash) {
super(hash);
this.extendedContext = new Context(new HashMap());
this.extendedContext.resolver = parent.resolver;
this.parent = parent;
this.data = parent.data;
this.resolver = parent.resolver;
}
@Override
public Object get(final List path) {
String key = path.get(0).toString();
// we must resolve this to parent context on block contexts (so tricky)
if (path.size() == 1 && key.equals("this")) {
return parent.model;
}
// path variable should resolve from parent :S
if (key.startsWith(".") && path.size() > 1) {
return parent.get(path.subList(1, path.size()));
}
return super.get(path);
}
@Override
protected Context newChildContext(final Object model) {
return new ParentFirst(model);
}
}
/**
* Context that resolve variables against parent, or fallback to default/normal lookup.
*
* @author edgar
* @since 3.0.0
*/
private static class ParentFirst extends Context {
/**
* Parent first lookup.
*
* @param model A model.
*/
protected ParentFirst(final Object model) {
super(model);
}
@Override
public Object get(final List path) {
Object value = parent.get(path);
if (value == null) {
return super.get(path);
}
return value;
}
@Override
protected Context newChildContext(final Object model) {
return new ParentFirst(model);
}
}
/**
* Partial context.
*
* @author edgar
* @since 4.0.5
*/
private static class PartialCtx extends Context {
/**
* Creates a new partial context.
*
* @param parent Parent.
* @param model Model.
* @param hash Hash.
*/
protected PartialCtx(final Context parent, final Object model, final Map hash) {
super(model);
this.extendedContext = new Context(hash);
this.extendedContext.resolver = parent.resolver;
this.extendedContext.extendedContext = new Context(Collections.emptyMap());
this.parent = parent;
this.data = parent.data;
this.resolver = parent.resolver;
}
@Override
public Object get(final List path) {
String key = path.get(0).toString();
// hash first, except for this
if (key.equals("this")) {
return super.get(path);
}
Object value = extendedContext.get(path);
if (value == null) {
return super.get(path);
}
return value;
}
}
/**
* A composite value resolver. It delegate the value resolution.
*
* @author edgar.espina
* @since 0.1.1
*/
private static class CompositeValueResolver implements ValueResolver {
/** The internal value resolvers. */
private List resolvers;
/**
* Creates a new {@link CompositeValueResolver}.
*
* @param resolvers The value resolvers.
*/
CompositeValueResolver(final List resolvers) {
this.resolvers = resolvers;
}
@Override
public Object resolve(final Object context, final String name) {
int i = 0;
while (i < resolvers.size()) {
Object value = resolvers.get(i).resolve(context, name);
if (value != UNRESOLVED) {
return value == null ? NULL : value;
}
i += 1;
}
return null;
}
@Override
public Object resolve(final Object context) {
int i = 0;
while (i < resolvers.size()) {
Object value = resolvers.get(i).resolve(context);
if (value != UNRESOLVED) {
return value == null ? NULL : value;
}
i += 1;
}
return null;
}
@Override
public Set> propertySet(final Object context) {
Set> propertySet = new LinkedHashSet<>();
for (ValueResolver resolver : resolvers) {
propertySet.addAll(resolver.propertySet(context));
}
return propertySet;
}
}
/**
* A context builder.
*
* @author edgar.espina
* @since 0.1.1
*/
public static final class Builder {
/** The context product. */
private Context context;
/**
* Creates a new context builder.
*
* @param parent The parent context. Required.
* @param model The model data.
*/
private Builder(final Context parent, final Object model) {
context = parent.newChild(model);
}
/**
* Creates a new context builder.
*
* @param model The model data.
*/
private Builder(final Object model) {
context = Context.root(model);
context.setResolver(new CompositeValueResolver(ValueResolver.defaultValueResolvers()));
}
/**
* Combine the given model using the specified name.
*
* @param name The variable's name. Required.
* @param model The model data.
* @return This builder.
*/
public Builder combine(final String name, final Object model) {
context.combine(name, model);
return this;
}
/**
* Combine all the map entries into the context stack.
*
* @param model The model data.
* @return This builder.
*/
public Builder combine(final Map model) {
context.combine(model);
return this;
}
/**
* Set the value resolvers to use.
*
* @param resolvers The value resolvers. Required.
* @return This builder.
*/
public Builder resolver(final ValueResolver... resolvers) {
notEmpty(resolvers, "At least one value-resolver must be present.");
boolean mapResolver = Stream.of(resolvers).anyMatch(MapValueResolver.class::isInstance);
if (!mapResolver) {
context.setResolver(
new CompositeValueResolver(
Stream.concat(Stream.of(resolvers), Stream.of(MapValueResolver.INSTANCE))
.collect(Collectors.toList())));
} else {
context.setResolver(new CompositeValueResolver(Arrays.asList(resolvers)));
}
return this;
}
/**
* Add one or more value resolver to the defaults defined by {@link
* ValueResolver#defaultValueResolvers()}.
*
* @param resolvers The value resolvers. Required.
* @return This builder.
*/
public Builder push(final ValueResolver... resolvers) {
notEmpty(resolvers, "At least one value-resolver must be present.");
List merged = new ArrayList<>();
merged.addAll(ValueResolver.defaultValueResolvers());
Stream.of(resolvers).forEach(merged::add);
context.setResolver(new CompositeValueResolver(merged));
return this;
}
/**
* Build a context stack.
*
* @return A new context stack.
*/
public Context build() {
return context;
}
}
/**
* Path expression chain.
*
* @author edgar
* @since 4.0.1
*/
private static class PathExpressionChain implements PathExpression.Chain {
/** Expression path. */
private List path;
/** Cursor to move/execute the next expression. */
private int i = 0;
/**
* Creates a new {@link PathExpressionChain}.
*
* @param path Expression path.
*/
PathExpressionChain(final List path) {
this.path = path;
}
@Override
public Object next(final ValueResolver resolver, final Context context, final Object data) {
if (data != null && i < path.size()) {
PathExpression next = path.get(i++);
return next.eval(resolver, context, data, this);
}
return data;
}
@Override
public List path() {
return path.subList(i, path.size());
}
/**
* Reset any previous state and restart the evaluation.
*
* @param resolver Value resolver.
* @param context Context object.
* @param data Data object.
* @return A resolved value or null
.
*/
public Object eval(final ValueResolver resolver, final Context context, final Object data) {
i = 0;
Object value = next(resolver, context, data);
if (value == null) {
return i > 1 ? NULL : null;
}
return value;
}
}
/** Mark for fail context lookup. */
private static final Object NULL = new Object();
/** The qualified name for partials. Internal use. */
public static final String PARTIALS = Context.class.getName() + "#partials";
/** Inline partials. */
public static final String INLINE_PARTIALS = "__inline_partials_";
/** The qualified name for partials. Internal use. */
public static final String INVOCATION_STACK = Context.class.getName() + "#invocationStack";
/** Number of parameters of a helper. Internal use. */
public static final String PARAM_SIZE = Context.class.getName() + "#paramSize";
/** Last callee of a partial block. Internal use. */
public static final String CALLEE = Context.class.getName() + "#callee";
/** The parent context. Optional. */
protected Context parent;
/** The target value. Resolved as '.' or 'this' inside templates. Required. */
Object model;
/** A thread safe storage. */
protected Map data;
/** Additional, data can be stored here. */
protected Context extendedContext;
/** The value resolver. */
protected ValueResolver resolver;
/**
* Creates a new context.
*
* @param model The target value. Resolved as '.' or 'this' inside templates. Required.
*/
protected Context(final Object model) {
this.model = model;
this.extendedContext = null;
this.parent = null;
}
/**
* Creates a root context.
*
* @param model The target value. Resolved as '.' or 'this' inside templates. Required.
* @return A root context.
*/
private static Context root(final Object model) {
Context root = new Context(model);
root.extendedContext = new Context(new HashMap());
root.data = new HashMap<>();
root.data.put(PARTIALS, new HashMap());
LinkedList