Java tutorial
/** * Copyright (c) 2012-2013 Edgar Espina * * This file is part of Handlebars.java. * * 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 com.github.jknack.handlebars; import static org.apache.commons.lang3.Validate.notEmpty; import static org.apache.commons.lang3.Validate.notNull; import java.lang.reflect.Array; 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.regex.Matcher; import java.util.regex.Pattern; import com.github.jknack.handlebars.io.TemplateSource; /** * Mustache/Handlebars are contextual template engines. This class represent the * 'context stack' of a template. * <ul> * <li>Objects and hashes should be pushed onto the context stack. * <li>All elements on the context stack should be accessible. * <li>Multiple sections per template should be permitted. * <li>Failed context lookups should be considered falsy. * <li>Dotted names should be valid for Section tags. * <li>Dotted names that cannot be resolved should be considered falsy. * <li>Dotted Names - Context Precedence: Dotted names should be resolved against former * resolutions. * </ul> * * @author edgar.espina * @since 0.1.0 */ public class Context { /** * Handlebars and Mustache path separator. */ private static final String PATH_SEPARATOR = "./"; /** * Handlebars 'parent' attribute reference. */ private static final String PARENT_ATTR = "../"; /** * Handlebars 'parent' attribute reference. */ private static final String PARENT = ".."; /** * Handlebars 'this' reference. */ private static final String THIS = "this"; /** * The mustache 'this' reference. */ private static final String MUSTACHE_THIS = "."; /** * 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 ValueResolver[] resolvers; /** * Creates a new {@link CompositeValueResolver}. * * @param resolvers The value resolvers. */ public CompositeValueResolver(final ValueResolver... resolvers) { this.resolvers = resolvers; } @Override public Object resolve(final Object context, final String name) { for (ValueResolver resolver : resolvers) { Object value = resolver.resolve(context, name); if (value != UNRESOLVED) { return value == null ? NULL : value; } } return null; } @Override public Object resolve(final Object context) { for (ValueResolver resolver : resolvers) { Object value = resolver.resolve(context); if (value != UNRESOLVED) { return value == null ? NULL : value; } } return null; } @Override public Set<Entry<String, Object>> propertySet(final Object context) { Set<Entry<String, Object>> propertySet = new LinkedHashSet<Map.Entry<String, Object>>(); 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 = Context.child(parent, model); } /** * Creates a new context builder. * * @param model The model data. */ private Builder(final Object model) { context = Context.root(model); } /** * 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<String, ?> 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."); context.setResolver(new CompositeValueResolver(resolvers)); return this; } /** * Build a context stack. * * @return A new context stack. */ public Context build() { if (context.resolver == null) { if (context.parent != null) { // Set resolver from parent. context.resolver = context.parent.resolver; } else { // Set default value resolvers: Java Bean like and Map resolvers. context.setResolver(new CompositeValueResolver(ValueResolver.VALUE_RESOLVERS)); } // Expand resolver to the extended context. if (context.extendedContext != null) { context.extendedContext.resolver = context.resolver; } } return context; } } /** * Mark for fail context lookup. */ private static final Object NULL = new Object(); /** * Property access expression. */ private static final Pattern IDX = Pattern.compile("\\[((.)+)\\]"); /** * Index access expression. */ private static final Pattern INT = Pattern.compile("\\d+"); /** * Parser for path expressions. */ private static final PropertyPathParser PATH_PARSER = new PropertyPathParser(PATH_SEPARATOR); /** * The qualified name for partials. Internal use. */ public static final String PARTIALS = Context.class.getName() + "#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"; /** * The parent context. Optional. */ private Context parent; /** * The target value. Resolved as '.' or 'this' inside templates. Required. */ private Object model; /** * A thread safe storage. */ private Map<String, Object> data; /** * Additional, data can be stored here. */ private Context extendedContext; /** * The value resolver. */ private ValueResolver resolver; /** * Creates a new context. * * @param model The target value. Resolved as '.' or 'this' inside * templates. Required. */ protected Context(final Object model) { if (model instanceof Context) { throw new IllegalArgumentException("Invalid model type:" + model.getClass().getName()); } this.model = model; } /** * 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<String, Object>()); root.parent = null; root.data = new HashMap<String, Object>(); root.data.put(PARTIALS, new HashMap<String, Template>()); root.data.put(INVOCATION_STACK, new LinkedList<TemplateSource>()); return root; } /** * Creates a child context. * * @param parent The parent context. Required. * @param model The target value. Resolved as '.' or 'this' inside * templates. Required. * @return A child context. */ private static Context child(final Context parent, final Object model) { notNull(parent, "A parent context is required."); Context child = new Context(model); child.extendedContext = new Context(new HashMap<String, Object>()); child.parent = parent; child.data = parent.data; return child; } /** * Insert a new attribute in the context-stack. * * @param name The attribute's name. Required. * @param model The model data. */ @SuppressWarnings({ "unchecked" }) private void combine(final String name, final Object model) { notEmpty(name, "The variable's name is required."); Map<String, Object> map = (Map<String, Object>) extendedContext.model; map.put(name, model); } /** * Inser all the attributes in the context-stack. * * @param model The model attributes. */ @SuppressWarnings({ "unchecked" }) private void combine(final Map<String, ?> model) { Map<String, Object> map = (Map<String, Object>) extendedContext.model; map.putAll(model); } /** * Read the attribute from the data storage. * * @param name The attribute's name. * @param <T> Data type. * @return The attribute value or null. */ @SuppressWarnings("unchecked") public <T> 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) { notEmpty(name, "The attribute's name is required."); 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<String, ?> attributes) { notNull(attributes, "The attributes are required."); data.putAll(attributes); return this; } /** * Resolved as '.' or 'this' inside templates. * * @return The model or data. */ public Object model() { return model; } /** * The parent context or null. * * @return The parent context or null. */ public 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<Entry<String, Object>> 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<Entry<String, Object>> propertySet() { return propertySet(model); } /** * Lookup the given key inside the context stack. * <ul> * <li>Objects and hashes should be pushed onto the context stack. * <li>All elements on the context stack should be accessible. * <li>Multiple sections per template should be permitted. * <li>Failed context lookups should be considered falsey. * <li>Dotted names should be valid for Section tags. * <li>Dotted names that cannot be resolved should be considered falsey. * <li>Dotted Names - Context Precedence: Dotted names should be resolved against former * resolutions. * </ul> * * @param key The object key. * @return The value associated to the given key or <code>null</code> if no * value is found. */ public Object get(final String key) { // '.' or 'this' if (MUSTACHE_THIS.equals(key) || THIS.equals(key)) { return internalGet(model); } // '..' if (key.equals(PARENT)) { return parent == null ? null : internalGet(parent.model); } // '../' if (key.startsWith(PARENT_ATTR)) { return parent == null ? null : parent.get(key.substring(PARENT_ATTR.length())); } String[] path = toPath(key); Object value = internalGet(path); if (value == null) { // No luck, check the extended context. value = get(extendedContext, key); // No luck, check the data context. if (value == null && data != null) { String dataKey = key.charAt(0) == '@' ? key.substring(1) : key; // simple data keys will be resolved immediately, complex keys need to go down and using a // new context. value = data.get(dataKey); if (value == null && path.length > 1) { // for complex keys, a new data context need to be created per invocation, // bc data might changes per execution. Context dataContext = Context.newBuilder(data).resolver(this.resolver).build(); // don't extend the lookup further. dataContext.data = null; value = dataContext.get(dataKey); // destroy it! dataContext.destroy(); } } // No luck, but before checking at the parent scope we need to check for // the 'this' qualifier. If present, no look up will be done. if (value == null && !path[0].equals(THIS)) { value = get(parent, key); } } return value == NULL ? null : value; } /** * Look for the specified key in an external context. * * @param external The external context. * @param key The associated key. * @return The associated value or null if not found. */ private Object get(final Context external, final String key) { return external == null ? null : external.get(key); } /** * Split the property name by '.' (except within an escaped blocked) and create an array of it. * * @param key The property's name. * @return A path representation of the property (array based). */ private String[] toPath(final String key) { return PATH_PARSER.parsePath(key); } /** * @param candidate resolve a candidate object. * @return A resolved value or the current value if there isn't a resolved value. */ private Object internalGet(final Object candidate) { Object resolved = resolver.resolve(candidate); return resolved == null ? candidate : resolved; } /** * Iterate over the qualified path and return a value. The value can be * null, {@link #NULL} or not null. If the value is <code>null</code>, the * value isn't present and the lookup algorithm will searchin for the value in * the parent context. * If the value is {@value #NULL} the search must stop bc the context for * the given path exists but there isn't a value there. * * @param path The qualified path. * @return The value inside the stack for the given path. */ private Object internalGet(final String... path) { Object current = model; // Resolve 'this' to the current model. int start = path[0].equals(THIS) ? 1 : 0; for (int i = start; i < path.length - 1; i++) { current = resolve(current, path[i]); if (current == null) { return null; } } String name = path[path.length - 1]; Object value = resolve(current, name); if (value == null && current != model) { // We're looking in the right scope, but the value isn't there // returns a custom mark to stop looking value = NULL; } return value; } /** * Do the actual lookup of an unqualified property name. * * @param current The target object. * @param expression The access expression. * @return The associated value. */ private Object resolve(final Object current, final String expression) { // Null => null if (current == null) { return null; } // array/list access or invalid Java identifiers wrapped with [] Matcher matcher = IDX.matcher(expression); if (matcher.matches()) { String idx = matcher.group(1); if (INT.matcher(idx).matches()) { Object result = resolveArrayAccess(current, idx); if (result != NULL) { return result; } } // It is not a index base object, defaults to string property lookup // (usually not a valid Java identifier) return resolver.resolve(current, idx); } // array or list access, exclusive if (INT.matcher(expression).matches()) { Object result = resolveArrayAccess(current, expression); if (result != NULL) { return result; } } return resolver.resolve(current, expression); } /** * Resolve a array or list access using idx. * * @param current The current scope. * @param idx The index of the array or list. * @return An object at the given location or null. */ @SuppressWarnings("rawtypes") private Object resolveArrayAccess(final Object current, final String idx) { // It is a number, check if the current value is a index base object. int pos = Integer.parseInt(idx); try { if (current instanceof List) { return ((List) current).get(pos); } else if (current.getClass().isArray()) { return Array.get(current, pos); } } catch (IndexOutOfBoundsException exception) { // Index is outside of range, fallback to null as in handlebar.js return null; } return NULL; } /** * 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) { notNull(parent, "The parent context is required."); 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 root context. * * @param model The model data. * @return A new root context. */ public static Context newContext(final Object model) { return newBuilder(model).build(); } }