/* * Copyright 2013 Martin Kouba * * 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 org.trimou.handlebars; import static org.trimou.handlebars.OptionsHashKeys.APPLY; import static org.trimou.handlebars.OptionsHashKeys.AS; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; import org.trimou.engine.config.EngineConfigurationKey; import org.trimou.engine.segment.ImmutableIterationMeta; import org.trimou.exception.MustacheException; import org.trimou.exception.MustacheProblem; import org.trimou.util.ImmutableSet; import org.trimou.util.Iterables; /** * <code> * {{#each items}} * {{name}} * {{/each}} * </code> * * <p> * It's possible to apply a function to each element. The function must be an * instance of {@link Function}. Note that the function cannot be type-safe. If * the result does not equal to {@link EachHelper#SKIP_RESULT} it's used instead * of the original element. If the result equals to * {@link EachHelper#SKIP_RESULT} the element is skipped. This might be useful * to filter out unnecessary elements or to wrap/transform elements. * </p> * * <code> * {{#each items apply=myFunction}} * {{name}} * {{/each}} * </code> * * <p> * It's also possible to supply an alias to access the value of the current * iteration: * </p> * * <code> * {{#each items as='item'}} * {{item.name}} * {{/each}} * </code> * * <p> * This helper could be used to iterate over multiple objects: * <p> * * <code> * {{! First iterate over list1 and then iterate over list2}} * {{#each list1 list2}} * {{name}} * {{/each}} * </code> * * @see Function * @author Martin Kouba */ public class EachHelper extends BasicSectionHelper { public static final String SKIP_RESULT = "org.trimou.handlebars.skipResult"; private String iterationMetadataAlias; @Override public void init() { super.init(); this.iterationMetadataAlias = configuration.getStringPropertyValue( EngineConfigurationKey.ITERATION_METADATA_ALIAS); } @Override public void execute(Options options) { if (options.getParameters().size() == 1) { Object param = options.getParameters().get(0); if (param == null) { // Treat null values as empty objects return; } processParameter(param, options, 1, getSize(param)); } else { int size = 0; int index = 1; List<Object> params = new ArrayList<>(options.getParameters()); for (Iterator<Object> iterator = params.iterator(); iterator .hasNext();) { Object param = iterator.next(); int paramSize = 0; if (param != null) { paramSize = getSize(param); } if (paramSize > 0) { size += paramSize; } else { // Treat null values as empty objects iterator.remove(); } } if (size == 0) { return; } for (Object param : params) { index = processParameter(param, options, index, size); } } } @Override protected Set<String> getSupportedHashKeys() { return ImmutableSet.of(APPLY, AS); } private int processParameter(Object param, Options options, int index, int size) { if (param instanceof Iterable) { return processIterable((Iterable<?>) param, options, index, size); } else if (param.getClass().isArray()) { return processArray(param, options, index, size); } else { throw new MustacheException( MustacheProblem.RENDER_HELPER_INVALID_OPTIONS, "%s is nor an Iterable nor an array [%s]", param, options.getTagInfo()); } } private int processIterable(Iterable<?> iterable, Options options, int index, int size) { Iterator<?> iterator = iterable.iterator(); Function function = initFunction(options); String alias = initValueAlias(options); while (iterator.hasNext()) { nextElement(options, iterator.next(), size, index++, function, alias); } return index; } private int processArray(Object array, Options options, int index, int size) { int length = Array.getLength(array); Function function = initFunction(options); String alias = initValueAlias(options); for (int i = 0; i < length; i++) { nextElement(options, Array.get(array, i), size, index++, function, alias); } return index; } private int getSize(Object param) { if (param instanceof Iterable) { return Iterables.size((Iterable<?>) param); } else if (param.getClass().isArray()) { return Array.getLength(param); } return 0; } private void nextElement(Options options, Object value, int size, int index, Function function, String valueAlias) { if (function != null) { value = function.apply(value); if (SKIP_RESULT.equals(value)) { return; } } if (valueAlias != null) { options.push(new ImmutableIterationMeta(iterationMetadataAlias, size, index, valueAlias, value)); options.fn(); options.pop(); } else { options.push(new ImmutableIterationMeta(iterationMetadataAlias, size, index)); options.push(value); options.fn(); options.pop(); options.pop(); } } private Function initFunction(Options options) { Object function = options.getHash().get(APPLY); if (function == null) { return null; } if (function instanceof Function) { return (Function) function; } throw new MustacheException( MustacheProblem.RENDER_HELPER_INVALID_OPTIONS, "%s is not a valid function [%s]", function, options.getTagInfo()); } private String initValueAlias(Options options) { Object as = options.getHash().get(AS); if (as == null) { return null; } return as.toString(); } }