@Test public void pushResolver() throws IOException { ValueResolver resolver = new ValueResolver() { @Override public Object resolve(final Object context) { return 1; } @Override public Object resolve(final Object context, final String name) { return 1; } @Override public Set<Entry<String, Object>> propertySet(final Object context) { return null; } }; Map<String, Object> hash = new HashMap<>(); hash.put("foo", "bar"); Context ctx = Context.newBuilder(hash).push(resolver).build(); assertEquals("bar", ctx.get("foo")); assertEquals(1, ctx.get("bar")); }
/** * 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 model; } // '..' if (key.equals(PARENT)) { return parent == null ? null : parent.model; } // '../' if (key.startsWith(PARENT_ATTR)) { return parent == null ? null : parent.get(key.substring(PARENT_ATTR.length())); } String[] path = toPath(key); Object value = get(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(MapValueResolver.INSTANCE).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; }
@SuppressWarnings("unchecked") @Override protected void merge(final Context context, final Writer writer) throws IOException { if (body == null) { return; } final String helperName; Helper<Object> helper = helper(name); Template template = body; final Object childContext; Context currentScope = context; if (helper == null) { childContext = transform(context.get(name)); if (inverted) { helperName = UnlessHelper.NAME; } else if (childContext instanceof Iterable) { helperName = EachHelper.NAME; } else if (childContext instanceof Boolean) { helperName = IfHelper.NAME; } else if (childContext instanceof Lambda) { helperName = WithHelper.NAME; template = Lambdas.compile( handlebars, (Lambda<Object, Object>) childContext, context, template, startDelimiter, endDelimiter); } else { helperName = WithHelper.NAME; currentScope = Context.newContext(context, childContext); } // A built-in helper might be override it. helper = handlebars.helper(helperName); } else { helperName = name; childContext = transform(determineContext(context)); } Options options = new Options.Builder(handlebars, helperName, TagType.SECTION, currentScope, template) .setInverse(inverse == null ? Template.EMPTY : inverse) .setParams(params(currentScope)) .setHash(hash(context)) .build(); options.data(Context.PARAM_SIZE, this.params.size()); CharSequence result = helper.apply(childContext, options); if (!isEmpty(result)) { writer.append(result); } }
@Override public CharSequence apply(final Object context, final Options options) throws IOException { if (context == null) { return null; } if (options.params.length <= 0) { return context.toString(); } Context ctx = Context.newBuilder(options.context, context).build(); Object lookup = ctx.get(options.param(0).toString()); if (lookup == null) { return null; } return lookup.toString(); }
/** * 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) { if (".".equals(key) || "this".equals(key)) { return model; } if (key.startsWith("../")) { return parent == null ? null : parent.get(key.substring("../".length())); } String[] path = toPath(key); Object value = get(path); if (value == null) { // No luck, check the extended context. value = get(extendedContext, key); // 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); }
@Override Object doParse(final Context scope, final Object param) { return scope.get((String) param); }