All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jknack.handlebars.Context Maven / Gradle / Ivy

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> partials = new LinkedList<>(); partials.push(new HashMap()); root.data.put(INLINE_PARTIALS, partials); root.data.put(INVOCATION_STACK, new LinkedList()); root.data.put("root", model); return root; } /** * Insert a new attribute in the context-stack. * * @param name The attribute's name. Required. * @param model The model data. * @return This context. */ @SuppressWarnings({"unchecked"}) public Context combine(final String name, final Object model) { Map map = (Map) extendedContext.model; map.put(name, model); return this; } /** * Insert all the attributes in the context-stack. * * @param model The model attributes. * @return This context. */ @SuppressWarnings({"unchecked"}) public Context combine(final Map model) { Map map = (Map) extendedContext.model; map.putAll(model); return this; } /** * Read the attribute from the data storage. * * @param name The attribute's name. * @param Data type. * @return The attribute value or null. */ @SuppressWarnings("unchecked") public T data(final String name) { return (T) data.get(name); } /** * Set an attribute in the data storage. * * @param name The attribute's name. Required. * @param value The attribute's value. Required. * @return This context. */ public Context data(final String name, final Object value) { data.put(name, value); return this; } /** * Store the map in the data storage. * * @param attributes The attributes to add. Required. * @return This context. */ public Context data(final Map attributes) { data.putAll(attributes); return this; } /** * Resolved as '.' or 'this' inside templates. * * @return The model or data. */ public final Object model() { return model; } /** * The parent context or null. * * @return The parent context or null. */ public final Context parent() { return parent; } /** * List all the properties and values for the given object. * * @param context The context object. * @return All the properties and values for the given object. */ public Set> propertySet(final Object context) { if (context == null) { return Collections.emptySet(); } if (context instanceof Context) { return resolver.propertySet(((Context) context).model); } return resolver.propertySet(context); } /** * List all the properties and values of {@link #model()}. * * @return All the properties and values of {@link #model()}. */ public Set> propertySet() { return propertySet(model); } /** * @return True, if this context is a block param context. */ public boolean isBlockParams() { return this instanceof BlockParam; } /** * Lookup the given key inside the context stack. * *
    *
  • 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 falsey. *
  • Dotted names should be valid for Section tags. *
  • Dotted names that cannot be resolved should be considered falsey. *
  • Dotted Names - Context Precedence: Dotted names should be resolved against former * resolutions. *
* * @param path The object path. * @return The value associated to the given key or null if no value is found. */ public Object get(final List path) { PathExpression head = path.get(0); boolean local = head.local(); Object value = null; PathExpressionChain expr = new PathExpressionChain(path); Context it = this; if (local) { value = expr.next(resolver, it, it.model); // extends local lookup to extended context if 'this' isn't present if (value == null && !(head instanceof ThisPath)) { value = expr.eval(resolver, it.extendedContext, it.extendedContext.model); } } else { while (value == null && it != null) { value = expr.eval(resolver, it, it.model); if (value == null) { // No luck, check the extended context. value = expr.eval(resolver, it.extendedContext, it.extendedContext.model); if (value == null) { // data context value = expr.eval(resolver, it, it.data); } } it = it.parent; } } return value == NULL ? null : value; } /** * Lookup the given key inside the context stack. * *
    *
  • 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 falsey. *
  • Dotted names should be valid for Section tags. *
  • Dotted names that cannot be resolved should be considered falsey. *
  • Dotted Names - Context Precedence: Dotted names should be resolved against former * resolutions. *
* * @param key The object key. * @return The value associated to the given key or null if no value is found. */ public Object get(final String key) { return get(key, true); } /** * Lookup the given key inside the context stack. * *
    *
  • 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 falsey. *
  • Dotted names should be valid for Section tags. *
  • Dotted names that cannot be resolved should be considered falsey. *
  • Dotted Names - Context Precedence: Dotted names should be resolved against former * resolutions. *
* * @param key The object key. * @param parentScopeResolution False, if we want to restrict lookup to current scope. * @return The value associated to the given key or null if no value is found. */ public Object get(final String key, final boolean parentScopeResolution) { return get(PathCompiler.compile(key, parentScopeResolution)); } /** * Set the value resolver and propagate it to the extendedContext. * * @param resolver The value resolver. */ private void setResolver(final ValueResolver resolver) { this.resolver = resolver; extendedContext.resolver = resolver; } /** Destroy this context by cleaning up instance attributes. */ public void destroy() { model = null; if (parent == null) { // Root context is the owner of the storage. if (data != null) { data.clear(); } } if (extendedContext != null) { extendedContext.destroy(); } parent = null; resolver = null; data = null; } @Override public String toString() { return String.valueOf(model); } /** * Start a new context builder. * * @param parent The parent context. Required. * @param model The model data. * @return A new context builder. */ public static Builder newBuilder(final Context parent, final Object model) { return new Builder(parent, model); } /** * Start a new context builder. * * @param model The model data. * @return A new context builder. */ public static Builder newBuilder(final Object model) { return new Builder(model); } /** * Creates a new child context. * * @param parent The parent context. Required. * @param model The model data. * @return A new child context. */ public static Context newContext(final Context parent, final Object model) { return newBuilder(parent, model).build(); } /** * Creates a new block param context. * * @param parent The parent context. Required. * @param names A list of names to set in the block param context. * @param values A list of values to set in the block param context. * @return A new block param context. */ public static Context newBlockParamContext( final Context parent, final List names, final List values) { Map hash = new HashMap<>(); for (int i = 0; i < Math.min(values.size(), names.size()); i++) { hash.put(names.get(i), values.get(i)); } return new BlockParam(parent, hash); } /** * Creates a new partial context. * * @param ctx Current scope. * @param scope Scope switch. * @param hash Partial hash. * @return A new context. */ public static Context newPartialContext( final Context ctx, final String scope, final Map hash) { return new PartialCtx(ctx, ctx.get(scope), hash); } /** * Creates a new root context. * * @param model The model data. * @return A new root context. */ public static Context newContext(final Object model) { return newBuilder(model).build(); } /** * Creates a new child context. * * @param model A model/data. * @return A new context. */ private Context newChild(final Object model) { Context child = newChildContext(model); child.extendedContext = new Context(new HashMap()); child.setResolver(this.resolver); child.parent = this; child.data = this.data; return child; } /** * Creates an empty/default context. * * @param model A model/data. * @return A new context. */ protected Context newChildContext(final Object model) { return new Context(model); } /** * Creates a new context but keep the data attribute. * * @param context Context to extract the data attribute. * @param model A model/data. * @return A new context. */ public static Context copy(final Context context, final Object model) { Context ctx = Context.newContext(model); ctx.data = context.data; ctx.resolver = context.resolver; return ctx; } }