/*
 * 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.engine.segment;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.MustacheEngine;
import org.trimou.engine.MustacheTagInfo;
import org.trimou.engine.context.ExecutionContext;
import org.trimou.engine.context.ValueWrapper;
import org.trimou.engine.interpolation.LiteralSupport;
import org.trimou.engine.parser.Template;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.handlebars.Helper;
import org.trimou.handlebars.HelperDefinition;
import org.trimou.handlebars.HelperDefinition.ValuePlaceholder;
import org.trimou.handlebars.Options;
import org.trimou.util.Checker;
import org.trimou.util.ImmutableList;
import org.trimou.util.ImmutableList.ImmutableListBuilder;
import org.trimou.util.ImmutableMap;
import org.trimou.util.ImmutableMap.ImmutableMapBuilder;
import org.trimou.util.Strings;

/**
 * Wraps {@link Helper} instance and handles its execution (e.g. builds
 * {@link Options} instance).
 *
 * @author Martin Kouba
 * @see HelperAwareSegment
 */
class HelperExecutionHandler {

    private final Helper helper;

    private final OptionsBuilder optionsBuilder;

    /**
     *
     * @param helper
     * @param optionsBuilder
     */
    private HelperExecutionHandler(Helper helper,
            OptionsBuilder optionsBuilder) {
        this.helper = helper;
        this.optionsBuilder = optionsBuilder;
    }

    /**
     *
     * @param name
     * @param configuration
     * @param segment
     * @return a handler for the given name or null if no such
     *         helper exists
     */
    static HelperExecutionHandler from(String name, MustacheEngine engine,
            HelperAwareSegment segment) {

        // Split the name and detect unterminated literals
        Iterator<String> parts = splitHelperName(name, segment);

        Helper helper = engine.getConfiguration().getHelpers()
                .get(parts.next());

        if (helper == null) {
            // No helper with the given name found
            return null;
        }

        ImmutableListBuilder<Object> params = ImmutableList.builder();
        ImmutableMapBuilder<String, Object> hash = ImmutableMap.builder();
        LiteralSupport literalSupport = engine.getConfiguration()
                .getLiteralSupport();

        while (parts.hasNext()) {
            // Next part is a param or a hash entry
            String part = parts.next();
            if (Strings.isListLiteral(part)) {
                params.add(new ListValuePlaceholder(part, engine,
                        literalSupport, segment));
            } else {
                int equalsPosition = getFirstDeterminingEqualsCharPosition(
                        part);
                if (equalsPosition != -1) {
                    String value = part.substring(equalsPosition + 1,
                            part.length());
                    if (Strings.isListLiteral(value)) {
                        hash.put(part.substring(0, equalsPosition),
                                new ListValuePlaceholder(value, engine,
                                        literalSupport, segment));
                    } else {
                        hash.put(part.substring(0, equalsPosition),
                                getLiteralOrPlaceholder(value, engine, segment,
                                        literalSupport));
                    }
                } else {
                    params.add(getLiteralOrPlaceholder(part, engine, segment,
                            literalSupport));
                }
            }
        }

        OptionsBuilder optionsBuilder = new OptionsBuilder(params.build(),
                hash.build(), segment, engine);

        // Let the helper validate the tag definition
        helper.validate(optionsBuilder);

        return new HelperExecutionHandler(helper, optionsBuilder);
    }

    /**
     *
     * @param appendable
     * @param executionContext
     */
    Appendable execute(Appendable appendable,
            ExecutionContext executionContext) {
        final DefaultOptions options = optionsBuilder.build(appendable,
                executionContext);
        try {
            helper.execute(options);
            return options.getAppendable();
        } finally {
            options.release();
        }
    }

    /**
     * Extracts parts from an input string. This implementation is quite naive
     * and should be possibly rewritten. Note that we can't use a simple
     * splitter because of string literals may contain whitespace chars.
     *
     * @param name
     * @param segment
     * @return the parts of the helper name
     * @throws MustacheException
     *             If a compilation problem occurs
     */
    static Iterator<String> splitHelperName(String name, Segment segment) {

        boolean stringLiteral = false;
        boolean arrayLiteral = false;
        boolean space = false;
        List<String> parts = new ArrayList<>();
        StringBuilder buffer = new StringBuilder();

        for (int i = 0; i < name.length(); i++) {
            if (name.charAt(i) == ' ') {
                if (!space) {
                    if (!stringLiteral && !arrayLiteral) {
                        if (buffer.length() > 0) {
                            parts.add(buffer.toString());
                            buffer = new StringBuilder();
                        }
                        space = true;
                    } else {
                        buffer.append(name.charAt(i));
                    }
                }
            } else {
                if (!arrayLiteral
                        && Strings.isStringLiteralSeparator(name.charAt(i))) {
                    stringLiteral = !stringLiteral;
                } else if (!stringLiteral
                        && Strings.isListLiteralStart(name.charAt(i))) {
                    arrayLiteral = true;
                } else if (!stringLiteral
                        && Strings.isListLiteralEnd(name.charAt(i))) {
                    arrayLiteral = false;
                }
                space = false;
                buffer.append(name.charAt(i));
            }
        }

        if (buffer.length() > 0) {
            if (stringLiteral || arrayLiteral) {
                throw new MustacheException(
                        MustacheProblem.COMPILE_HELPER_VALIDATION_FAILURE,
                        "Unterminated string or array literal detected: %s",
                        segment);
            }
            parts.add(buffer.toString());
        }
        return parts.iterator();
    }

    /**
     *
     * @param part
     * @return the index of an equals char outside of any string literal,
     *         -1 if no such char is found
     */
    static int getFirstDeterminingEqualsCharPosition(String part) {
        boolean stringLiteral = false;
        for (int i = 0; i < part.length(); i++) {
            if (Strings.isStringLiteralSeparator(part.charAt(i))) {
                if (i == 0) {
                    // The first char is a string literal separator
                    return -1;
                }
                stringLiteral = !stringLiteral;
            } else {
                if (!stringLiteral && part.charAt(i) == '=') {
                    return i;
                }
            }
        }
        return -1;
    }

    private static Object getLiteralOrPlaceholder(String value,
            MustacheEngine engine, HelperAwareSegment segment,
            LiteralSupport literalSupport) {
        Object literal = literalSupport.getLiteral(value, segment.getTagInfo());
        return literal != null ? literal
                : new DefaultValuePlaceholder(value, engine);
    }

    private static class OptionsBuilder implements HelperDefinition {

        private final List<Object> parameters;

        private final Map<String, Object> hash;

        private final HelperAwareSegment segment;

        private final MustacheEngine engine;

        // true if no placeholder found, also if params list is empty
        private final boolean isParamValuePlaceholderFound;

        // true if no placeholder found, also if hash map is empty
        private final boolean isHashValuePlaceholderFound;

        private OptionsBuilder(List<Object> parameters,
                Map<String, Object> hash, HelperAwareSegment segment,
                MustacheEngine engine) {
            this.parameters = parameters;
            this.hash = hash;
            this.segment = segment;
            this.engine = engine;
            this.isParamValuePlaceholderFound = initParamValuePlaceholderFound(
                    parameters);
            this.isHashValuePlaceholderFound = initHashValuePlaceholderFound(
                    hash);
        }

        @Override
        public MustacheTagInfo getTagInfo() {
            return segment.getTagInfo();
        }

        @Override
        public List<Object> getParameters() {
            return parameters;
        }

        @Override
        public Map<String, Object> getHash() {
            return hash;
        }

        @Override
        public String getContentLiteralBlock() {
            if (segment instanceof ContainerSegment) {
                return ((ContainerSegment) segment).getContentLiteralBlock();
            } else {
                return Strings.EMPTY;
            }
        }

        public DefaultOptions build(Appendable appendable,
                ExecutionContext executionContext) {
            List<ValueWrapper> valueWrappers = isParamValuePlaceholderFound
                    || isHashValuePlaceholderFound ? new LinkedList<>() : null;
            return new DefaultOptions(appendable, executionContext, segment,
                    getFinalParameters(executionContext, valueWrappers),
                    getFinalHash(executionContext, valueWrappers),
                    valueWrappers, engine, this);
        }

        private List<Object> getFinalParameters(
                ExecutionContext executionContext,
                List<ValueWrapper> valueWrappers) {
            if (isParamValuePlaceholderFound) {
                // At this point parameters list is never empty
                int size = parameters.size();
                switch (size) {
                case 1:
                    // Very often there will be only single param
                    return Collections
                            .singletonList(resolveValue(parameters.get(0),
                                    valueWrappers, executionContext));
                default:
                    List<Object> finalParams = new ArrayList<>(size);
                    for (Object param : parameters) {
                        finalParams.add(resolveValue(param, valueWrappers,
                                executionContext));
                    }
                    return Collections.unmodifiableList(finalParams);
                }
            } else {
                return parameters;
            }
        }

        private Map<String, Object> getFinalHash(
                ExecutionContext executionContext,
                List<ValueWrapper> valueWrappers) {
            if (isHashValuePlaceholderFound) {
                // At this point hash map is never empty
                int size = hash.size();
                switch (size) {
                case 1:
                    Entry<String, Object> singleEntry = hash.entrySet()
                            .iterator().next();
                    return Collections.singletonMap(singleEntry.getKey(),
                            resolveValue(singleEntry.getValue(), valueWrappers,
                                    executionContext));
                default:
                    Map<String, Object> finalHash = new HashMap<>();
                    for (Entry<String, Object> entry : hash.entrySet()) {
                        finalHash.put(entry.getKey(),
                                resolveValue(entry.getValue(), valueWrappers,
                                        executionContext));
                    }
                    return Collections.unmodifiableMap(finalHash);
                }
            } else {
                return hash;
            }
        }

        private Object resolveValue(Object value,
                List<ValueWrapper> valueWrappers,
                ExecutionContext executionContext) {
            if (value instanceof ValuePlaceholder) {
                if (value instanceof ListValuePlaceholder) {
                    ListValuePlaceholder listValues = (ListValuePlaceholder) value;
                    if (listValues.hasValuePlaceholderElement) {
                        ImmutableListBuilder<Object> builder = ImmutableList
                                .builder();
                        for (Object element : listValues) {
                            builder.add(resolveValue(element, valueWrappers,
                                    executionContext));
                        }
                        return builder.build();
                    } else {
                        // Values are immutable
                        return listValues.getValues();
                    }
                } else {
                    final ValueWrapper wrapper;
                    if (value instanceof DefaultValuePlaceholder) {
                        wrapper = ((DefaultValuePlaceholder) value)
                                .getProvider().get(executionContext);
                    } else {
                        wrapper = executionContext
                                .getValue(((ValuePlaceholder) value).getName());
                    }
                    valueWrappers.add(wrapper);
                    return wrapper.get();
                }
            } else {
                return value;
            }
        }

        private boolean initParamValuePlaceholderFound(
                List<Object> parameters) {
            if (parameters.isEmpty()) {
                return false;
            }
            for (Object param : parameters) {
                if (param instanceof ValuePlaceholder) {
                    return true;
                }
            }
            return false;
        }

        private boolean initHashValuePlaceholderFound(
                Map<String, Object> hash) {
            if (hash.isEmpty()) {
                return false;
            }
            for (Entry<String, Object> entry : hash.entrySet()) {
                if (entry.getValue() instanceof ValuePlaceholder) {
                    return true;
                }
            }
            return false;
        }

    }

    private static class DefaultOptions implements Options {

        private static final Logger LOGGER = LoggerFactory
                .getLogger(DefaultOptions.class);

        protected List<ValueWrapper> valueWrappers;

        protected Appendable appendable;

        protected int pushed;

        protected ExecutionContext executionContext;

        private final MustacheEngine engine;

        private final HelperAwareSegment segment;

        private final List<Object> parameters;

        private final Map<String, Object> hash;

        private final HelperDefinition originalDefinition;

        /**
         *
         * @param appendable
         * @param executionContext
         * @param segment
         * @param parameters
         * @param hash
         * @param valueWrappers
         * @param engine
         * @param originalDefinition
         */
        DefaultOptions(Appendable appendable, ExecutionContext executionContext,
                HelperAwareSegment segment, List<Object> parameters,
                Map<String, Object> hash, List<ValueWrapper> valueWrappers,
                MustacheEngine engine, HelperDefinition originalDefinition) {
            this.appendable = appendable;
            this.valueWrappers = valueWrappers;
            this.executionContext = executionContext;
            this.pushed = 0;
            this.segment = segment;
            this.parameters = parameters;
            this.hash = hash;
            this.engine = engine;
            this.originalDefinition = originalDefinition;
        }

        @Override
        public List<Object> getParameters() {
            return parameters;
        }

        @Override
        public Map<String, Object> getHash() {
            return hash;
        }

        @Override
        public void append(CharSequence sequence) {
            try {
                appendable.append(sequence);
            } catch (IOException e) {
                throw new MustacheException(MustacheProblem.RENDER_IO_ERROR, e);
            }
        }

        @Override
        public void fn() {
            appendable = segment.fn(appendable, executionContext);
        }

        @Override
        public void partial(String templateId) {
            partial(templateId, appendable);
        }

        @Override
        public void push(Object contextObject) {
            pushed++;
            executionContext = executionContext.setContextObject(contextObject);
        }

        @Override
        public Object pop() {
            if (pushed > 0) {
                pushed--;
                Object top = executionContext.getFirstContextObject();
                executionContext = executionContext.getParent();
                return top;
            }
            throw new MustacheException(
                    MustacheProblem.RENDER_HELPER_INVALID_POP_OPERATION);
        }

        @Override
        public Object peek() {
            return executionContext.getFirstContextObject();
        }

        @Override
        public Object getValue(String key) {
            if (valueWrappers == null) {
                valueWrappers = new ArrayList<>(5);
            }
            ValueWrapper wrapper = executionContext.getValue(key);
            valueWrappers.add(wrapper);
            return wrapper.get();
        }

        @Override
        public void partial(String templateId, Appendable appendable) {
            partial(templateId, appendable, executionContext);
        }

        @Override
        public void executeAsync(final HelperExecutable executable) {
            // For async execution we need to wrap the original appendable
            final AsyncAppendable asyncAppendable = new AsyncAppendable(
                    appendable);

            // Now submit the executable and get the future
            ExecutorService executor = engine.getConfiguration()
                    .geExecutorService();
            if (executor == null) {
                throw new MustacheException(
                        MustacheProblem.RENDER_ASYNC_PROCESSING_ERROR,
                        "ExecutorService must be set in order to submit an asynchronous task");
            }
            Future<AsyncAppendable> future = executor
                    .submit(() -> {
                        // We need a separate appendable for the async
                        // execution
                        DefaultOptions asyncOptions = new DefaultOptions(
                                new AsyncAppendable(asyncAppendable),
                                executionContext, segment, parameters, hash,
                                new ArrayList<>(), engine, originalDefinition);
                        executable.execute(asyncOptions);
                        return (AsyncAppendable) asyncOptions
                                .getAppendable();
                    });
            asyncAppendable.setFuture(future);
            this.appendable = asyncAppendable;
        }

        @Override
        public String source(String templateId) {
            Checker.checkArgumentNotEmpty(templateId);

            String mustacheSource = engine.getMustacheSource(templateId);

            if (mustacheSource == null) {
                throw new MustacheException(
                        MustacheProblem.RENDER_INVALID_PARTIAL_KEY,
                        "No mustache template found for the given key: %s %s",
                        templateId, segment.getOrigin());
            }
            return mustacheSource;
        }

        @Override
        public Appendable getAppendable() {
            return appendable;
        }

        @Override
        public void fn(Appendable appendable) {
            segment.fn(appendable, executionContext);
        }

        @Override
        public MustacheTagInfo getTagInfo() {
            return segment.getTagInfo();
        }

        @Override
        public String getContentLiteralBlock() {
            if (segment instanceof ContainerSegment) {
                return ((ContainerSegment) segment).getContentLiteralBlock();
            } else {
                return Strings.EMPTY;
            }
        }

        @Override
        public HelperDefinition getOriginalDefinition() {
            return originalDefinition;
        }

        protected void partial(String templateId, Appendable appendable,
                ExecutionContext executionContext) {
            Checker.checkArgumentsNotNull(templateId, appendable);

            Template partialTemplate = Segments.lookupTemplate(templateId,
                    engine, segment.getOrigin().getTemplate());

            if (partialTemplate == null) {
                throw new MustacheException(
                        MustacheProblem.RENDER_INVALID_PARTIAL_KEY,
                        "No partial found for the given key: %s %s", templateId,
                        segment.getOrigin());
            }
            // Note that indentation is not supported
            partialTemplate.getRootSegment().execute(appendable,
                    executionContext);
        }

        void release() {
            if (valueWrappers != null) {
                int wrappersSize = valueWrappers.size();
                if (wrappersSize == 1) {
                    valueWrappers.get(0).release();
                } else if (wrappersSize > 1) {
                    for (ValueWrapper wrapper : valueWrappers) {
                        wrapper.release();
                    }
                }
            }
            if (pushed > 0) {
                LOGGER.info(
                        "{} remaining objects pushed on the context stack will be automatically garbage collected [helperName: {}, template: {}]",
                        pushed, splitHelperName(segment.getTagInfo().getText(), segment).next(),
                        segment.getTagInfo().getTemplateName());
            }
        }

    }

    private static class DefaultValuePlaceholder implements ValuePlaceholder {

        private final String name;

        private final ValueProvider provider;

        DefaultValuePlaceholder(String name, MustacheEngine engine) {
            this.name = name;
            this.provider = new ValueProvider(name, engine.getConfiguration());
        }

        public String getName() {
            return name;
        }

        ValueProvider getProvider() {
            return provider;
        }

    }

    private static class ListValuePlaceholder
            implements ValuePlaceholder, Iterable<Object> {

        private final boolean hasValuePlaceholderElement;

        private final List<Object> values;

        private final String name;

        ListValuePlaceholder(String value, MustacheEngine engine,
                LiteralSupport literalSupport, HelperAwareSegment segment) {
            List<String> elements = Strings
                    .split(value.substring(1, value.length() - 1), ",");
            ImmutableListBuilder<Object> builder = ImmutableList.builder();
            for (String element : elements) {
                builder.add(getLiteralOrPlaceholder(element.trim(), engine, segment,
                        literalSupport));
            }
            this.values = builder.build();
            this.hasValuePlaceholderElement = initHasValuePlaceholderElement();
            this.name = value;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public Iterator<Object> iterator() {
            return values.iterator();
        }

        List<Object> getValues() {
            return values;
        }

        private boolean initHasValuePlaceholderElement() {
            for (Object val : values) {
                if (val instanceof ValuePlaceholder) {
                    return true;
                }
            }
            return false;
        }

    }

}


/*
 * Copyright 2016 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.el;

import java.beans.FeatureDescriptor;
import java.util.Iterator;

import javax.el.ELContext;
import javax.el.ELProcessor;
import javax.el.ELResolver;

import org.trimou.engine.config.Configuration;
import org.trimou.handlebars.Options;

/**
 *
 * @author Martin Kouba
 */
class Expressions {

    private Expressions() {
    }

    /**
     * Note that we have to use a separate {@link ELProcessor} for each evaluation
     * as the {@link OptionsELResolver} may not be reused.
     *
     * @param expression
     * @param options
     * @param configuration
     * @return the result of the expression evaluation
     */
    static Object eval(String expression, Options options, Configuration configuration) {
        ELProcessorFactory elpFactory = (ELProcessorFactory) configuration
                .getPropertyValue(ELProcessorFactory.EL_PROCESSOR_FACTORY_KEY);
        ELProcessor elp = elpFactory.createELProcessor(configuration);
        elp.getELManager().addELResolver(new OptionsELResolver(options));
        return elp.eval(expression);
    }

    static class OptionsELResolver extends ELResolver {

        private final Options options;

        OptionsELResolver(Options options) {
            this.options = options;
        }

        @Override
        public Object getValue(ELContext context, Object base, Object property) {
            if (context == null) {
                throw new NullPointerException();
            }
            if (base == null) {
                Object value = options.getValue(property.toString());
                if (value != null) {
                    context.setPropertyResolved(true);
                    return value;
                }
            }
            return null;
        }

        @Override
        public Class<?> getType(ELContext context, Object base, Object property) {
            return null;
        }

        @Override
        public void setValue(ELContext context, Object base, Object property, Object value) {
        }

        @Override
        public boolean isReadOnly(ELContext context, Object base, Object property) {
            return false;
        }

        @Override
        public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
            return null;
        }

        @Override
        public Class<?> getCommonPropertyType(ELContext context, Object base) {
            return null;
        }

    }

}


/*
 * Copyright 2014 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 java.util.Map;

import org.trimou.engine.MustacheEngineBuilder;
import org.trimou.engine.config.EngineConfigurationKey;
import org.trimou.handlebars.AlternativesHelper.MaxSelector;
import org.trimou.handlebars.AlternativesHelper.MinSelector;
import org.trimou.handlebars.AlternativesHelper.Selector;
import org.trimou.handlebars.EmbedHelper.SourceProcessor;
import org.trimou.handlebars.EvalHelper.Notation;
import org.trimou.handlebars.NumericExpressionHelper.Operator;
import org.trimou.util.ImmutableMap;
import org.trimou.util.ImmutableMap.ImmutableMapBuilder;

/**
 * A simple builder for helpers map. It's useful when registering built-in/basic
 * helpers with sensible default names (each, if, etc.) - no need to explicitly
 * declare a helper name.
 *
 *

 * HelpersBuilder.empty().addSet().addIsEven().add("myHelperName", new MyHelper())
 *         .build();
 *

 *
 * @author Martin Kouba
 * @see MustacheEngineBuilder#registerHelpers(Map)
 * @see MustacheEngineBuilder#registerHelpers(Map, boolean)
 */
public final class HelpersBuilder {

    public static final String EACH = "each";

    public static final String IF = "if";

    public static final String UNLESS = "unless";

    public static final String WITH = "with";

    public static final String IS = "is";

    public static final String IS_EVEN = "isEven";

    public static final String IS_ODD = "isOdd";

    public static final String SET = "set";

    public static final String INCLUDE = "include";

    public static final String EMBED = "embed";

    public static final String IS_EQUAL = "isEq";

    public static final String IS_NOT_EQUAL = "isNotEq";

    public static final String IS_NULL = "isNull";

    public static final String IS_NOT_NULL = "isNotNull";

    public static final String CHOOSE = "choose";

    public static final String WHEN = "when";

    public static final String OTHERWISE = "otherwise";

    public static final String SWITCH = "switch";

    public static final String CASE = "case";

    public static final String DEFAULT = "default";

    public static final String JOIN = "join";

    public static final String EVAL = "eval";

    public static final String NUMERIC_EXPRESSION = "numExpr";

    public static final String ASYNC = "async";

    public static final String INVOKE = "invoke";

    public static final String ALT = "alt";

    public static final String MIN = "min";

    public static final String MAX = "max";

    public static final String CACHE = "cache";

    public static final String REPEAT = "repeat";

    public static final String FMT = "fmt";

    public static final String TAG = "tag";

    private final ImmutableMapBuilder<String, Helper> builder;

    private HelpersBuilder() {
        this.builder = ImmutableMap.builder();
    }

    /**
     * @param name
     * @param helper
     * @return self
     */
    public HelpersBuilder add(String name, Helper helper) {
        builder.put(name, helper);
        return this;
    }

    /**
     * Add an instance of {@link EachHelper} with the {@value #EACH} name.
     *
     * @return self
     */
    public HelpersBuilder addEach() {
        builder.put(EACH, new EachHelper());
        return this;
    }

    /**
     * Add an instance of {@link IfHelper} with the {@value #IF} name.
     *
     * @return self
     */
    public HelpersBuilder addIf() {
        builder.put(IF, new IfHelper());
        return this;
    }

    /**
     * Add an instance of {@link IfHelper} with the {@value #IF} name and the given else delimiters.
     *
     * @return self
     */
    public HelpersBuilder addIf(String elseStartDelimiter, String elseEndDelimiter) {
        builder.put(IF, new IfHelper(elseStartDelimiter, elseEndDelimiter));
        return this;
    }

    /**
     * Add an instance of {@link UnlessHelper} with the {@value #UNLESS} name.
     *
     * @return self
     */
    public HelpersBuilder addUnless() {
        builder.put(UNLESS, new UnlessHelper());
        return this;
    }

    /**
     * Add an instance of {@link WithHelper} with the {@value #WITH} name.
     *
     * @return self
     */
    public HelpersBuilder addWith() {
        builder.put(WITH, new WithHelper());
        return this;
    }

    /**
     * Add an instance of {@link IsHelper} with the {@value #IS} name.
     *
     * @return self
     */
    public HelpersBuilder addIs() {
        builder.put(IS, new IsHelper());
        return this;
    }

    /**
     * Add an instance of {@link NumberIsEvenHelper} with the {@value #IS_EVEN}
     * name.
     *
     * @return self
     */
    public HelpersBuilder addIsEven() {
        builder.put(IS_EVEN, new NumberIsEvenHelper());
        return this;
    }

    /**
     * Add an instance of {@link NumberIsOddHelper} with the {@value #IS_ODD}
     * name.
     *
     * @return self
     */
    public HelpersBuilder addIsOdd() {
        builder.put(IS_ODD, new NumberIsOddHelper());
        return this;
    }

    /**
     * Add an instance of {@link SetHelper} with the {@value #SET} name.
     *
     * @return self
     */
    public HelpersBuilder addSet() {
        builder.put(SET, new SetHelper());
        return this;
    }

    /**
     * Add an instance of {@link IncludeHelper} with the {@value #INCLUDE} name.
     *
     * @return self
     */
    public HelpersBuilder addInclude() {
        builder.put(INCLUDE, new IncludeHelper());
        return this;
    }

    /**
     * Add an instance of {@link EmbedHelper} with the {@value #EMBED} name.
     *
     * @return self
     */
    public HelpersBuilder addEmbed() {
        builder.put(EMBED, new EmbedHelper());
        return this;
    }

    /**
     * Add an instance of {@link EmbedHelper} with the {@value #EMBED} name and the source processor.
     *
     * @param processor
     * @return self
     */
    public HelpersBuilder addEmbed(SourceProcessor processor) {
        builder.put(EMBED, new EmbedHelper(processor));
        return this;
    }

    /**
     * Add an instance of {@link EqualsHelper} with the {@value #IS_EQUAL} name.
     *
     * @return self
     */
    public HelpersBuilder addIsEqual() {
        builder.put(IS_EQUAL, new EqualsHelper());
        return this;
    }

    /**
     * Add an instance of {@link EqualsHelper} which tests inequality with the
     * {@value #IS_NOT_EQUAL} name.
     *
     * @return self
     */
    public HelpersBuilder addIsNotEqual() {
        builder.put(IS_NOT_EQUAL, new EqualsHelper(true));
        return this;
    }

    /**
     * Add an instance of {@link NullCheckHelper} with the {@value #IS_NULL}
     * name.
     *
     * @return self
     */
    public HelpersBuilder addIsNull() {
        builder.put(IS_NULL, new NullCheckHelper());
        return this;
    }

    /**
     * Add an instance of {@link NullCheckHelper} which tests "not null" with
     * the {@value #IS_NOT_NULL} name.
     *
     * @return self
     */
    public HelpersBuilder addIsNotNull() {
        builder.put(IS_NOT_NULL, new NullCheckHelper(true));
        return this;
    }

    /**
     * Add an instance of {@link ChooseHelper} with the {@value #CHOOSE} name.
     * Also adds the dependent helpers.
     *
     * @return self
     */
    public HelpersBuilder addChoose() {
        builder.put(CHOOSE, new ChooseHelper());
        builder.put(WHEN, new ChooseHelper.WhenHelper());
        builder.put(OTHERWISE, new ChooseHelper.OtherwiseHelper());
        return this;
    }

    /**
     * Add an instance of {@link SwitchHelper} with the {@value #SWITCH} name.
     * Also adds the dependent helpers.
     *
     * @return self
     */
    public HelpersBuilder addSwitch() {
        return addSwitch(false);
    }

    /**
     * Add an instance of {@link SwitchHelper} with the {@value #SWITCH} name.
     * Also adds the dependent helpers.
     *
     * @param caseDefaultIsBreak
     *            If true the matching case helper terminates the flow by
     *            default.
     * @return self
     */
    public HelpersBuilder addSwitch(boolean caseDefaultIsBreak) {
        builder.put(SWITCH, new SwitchHelper());
        builder.put(CASE, new SwitchHelper.CaseHelper(caseDefaultIsBreak));
        builder.put(DEFAULT, new SwitchHelper.DefaultHelper());
        return this;
    }

    /**
     * Add an instance of {@link JoinHelper}.
     *
     * @return self
     */
    public HelpersBuilder addJoin() {
        builder.put(JOIN, new JoinHelper());
        return this;
    }

    /**
     * Add an instance of {@link EvalHelper}.
     *
     * @return self
     */
    public HelpersBuilder addEval() {
        builder.put(EVAL, new EvalHelper());
        return this;
    }

    /**
     * Add an instance of {@link EvalHelper}.
     *
     * @param notation
     * @return self
     */
    public HelpersBuilder addEval(Notation notation) {
        builder.put(EVAL, new EvalHelper(notation));
        return this;
    }

    /**
     * Add an instance of {@link NumericExpressionHelper}.
     *
     * @return self
     */
    public HelpersBuilder addNumExpr() {
        builder.put(NUMERIC_EXPRESSION, new NumericExpressionHelper());
        return this;
    }

    /**
     * Add an instance of {@link NumericExpressionHelper}.
     *
     * @param defaultOperator
     * @return self
     */
    public HelpersBuilder addNumExpr(Operator defaultOperator) {
        builder.put(NUMERIC_EXPRESSION, new NumericExpressionHelper(defaultOperator));
        return this;
    }

    /**
     * Add an instance of {@link AsyncHelper}.
     *
     * @return self
     */
    public HelpersBuilder addAsync() {
        builder.put(ASYNC, new AsyncHelper());
        return this;
    }

    /**
     * Add an instance of {@link InvokeHelper}.
     *
     * @return self
     */
    public HelpersBuilder addInvoke() {
        builder.put(INVOKE, new InvokeHelper());
        return this;
    }

    /**
     * Add an instance of {@link AlternativesHelper}.
     *
     * @return self
     */
    public HelpersBuilder addAlt() {
        builder.put(ALT, new AlternativesHelper());
        return this;
    }

    /**
     * Add an instance of {@link AlternativesHelper}.
     *
     * @param selector
     * @return self
     */
    public HelpersBuilder addAlt(Selector selector) {
        builder.put(ALT, new AlternativesHelper(selector));
        return this;
    }

    /**
     * Add an instance of {@link AlternativesHelper}.
     *
     * @return self
     * @see MinSelector
     */
    public HelpersBuilder addMin() {
        builder.put(MIN, new AlternativesHelper(new MinSelector()));
        return this;
    }

    /**
     * Add an instance of {@link AlternativesHelper}.
     *
     * @return self
     * @see MaxSelector
     */
    public HelpersBuilder addMax() {
        builder.put(MAX, new AlternativesHelper(new MaxSelector()));
        return this;
    }

    /**
     * Add an instance of {@link CacheHelper}.
     *
     * @return self
     * @see MaxSelector
     */
    public HelpersBuilder addCache() {
        builder.put(CACHE, new CacheHelper());
        return this;
    }

    /**
     * Add an instance of {@link RepeatHelper}.
     *
     * @return self
     */
    public HelpersBuilder addRepeat() {
        builder.put(REPEAT, new RepeatHelper());
        return this;
    }

    public HelpersBuilder addFmt() {
        builder.put(FMT, new FormatHelper());
        return this;
    }

    public HelpersBuilder addTag() {
        builder.put(TAG, new TagHelper());
        return this;
    }

    /**
     *
     * @return self
     */
    public HelpersBuilder addBuiltin() {
        addEach();
        addIf();
        addUnless();
        addWith();
        addIs();
        return this;
    }

    /**
     *
     * @return self
     */
    public HelpersBuilder addExtra() {
        addIsEven();
        addIsOdd();
        addSet();
        addInclude();
        addEmbed();
        addIsEqual();
        addIsNotEqual();
        addIsNull();
        addIsNotNull();
        addChoose();
        addSwitch();
        addJoin();
        addEval();
        addNumExpr();
        addAsync();
        addInvoke();
        addAlt();
        addMin();
        addMax();
        addCache();
        addRepeat();
        addFmt();
        addTag();
        return this;
    }

    /**
     *
     * @return the built map
     */
    public Map<String, Helper> build() {
        return builder.build();
    }

    /**
     *
     * @return a new empty builder
     */
    public static HelpersBuilder empty() {
        return new HelpersBuilder();
    }

    /**
     * Built-in helpers are registered automatically if
     * {@link EngineConfigurationKey#HANDLEBARS_SUPPORT_ENABLED} set to
     * true.
     *
     * @return a new builder, all built-in helpers are added automatically
     * @see HelpersBuilder#addBuiltin()
     */
    public static HelpersBuilder builtin() {
        return empty().addBuiltin();
    }

    /**
     *
     * @return a new builder, extra helpers are added automatically
     * @see HelpersBuilder#addExtra()
     */
    public static HelpersBuilder extra() {
        return empty().addExtra();
    }

    /**
     *
     * @return a new builder, all available helpers are addded automatically
     */
    public static HelpersBuilder all() {
        return builtin().addExtra();
    }

}


/*
 * Copyright 2018 Trimou team
 *
 * 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.engine.convert;

import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;

import org.trimou.engine.config.AbstractConfigurationAware;
import org.trimou.engine.resolver.Decorator;
import org.trimou.engine.resolver.Decorator.AbstractBuilder;
import org.trimou.engine.resolver.MapResolver;
import org.trimou.engine.resolver.ReflectionResolver;
import org.trimou.util.Checker;
import org.trimou.util.ImmutableMap;

/**
 * This converter allows you to decorate a context object, e.g. to add a computed
 * property. The following snippet would render: "ooF".
 *
 *

 * MustacheEngine engine = MustacheEngineBuilder.newBuilder()
 *         .addContextConverter(
 *                 decorate(String.class).compute("reverse", s -> new StringBuilder(s).reverse().toString()).build())
 *         .build();
 * engine.compileMustache("{{reverse}}").render("Foo");
 *

 *
 *

 * By default, the converter is applied if the runtime class of the context
 * object is assignable to the delegate type. A custom predicate can be used but
 * beware of generics pitfalls and {@link ClassCastException}s.
 *

 *
 *

 * Note that {@link ReflectionResolver} and {@link MapResolver} must be
 * registered in order to make it work.
 *

 *
 * @author Martin Kouba
 */
public class DecoratorConverter<T> extends AbstractConfigurationAware implements ContextConverter {

    /**
     * Returns a decorator converter builder for the specified delegate type.
     *
     * @param delegateType
     * @return a new builder instance
     */
    public static <T> Builder<T> decorate(Class<T> delegateType) {
        return decorate(o -> (delegateType.isAssignableFrom(Decorator.unwrap(o).getClass())));
    }

    /**
     * Returns a decorator converter builder with the specified predicate used to
     * test a context object.
     *
     * @param delegateType
     * @return a new builder instance
     */
    public static <T> Builder<T> decorate(Predicate<Object> test) {
        return new Builder<>(test);
    }

    private final Predicate<Object> test;

    private final String delegateKey;

    private final Map<String, Function<T, Object>> mappings;

    private DecoratorConverter(Predicate<Object> test, String delegateKey, Map<String, Function<T, Object>> mappings) {
        this.test = test;
        this.delegateKey = delegateKey;
        this.mappings = mappings;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object convert(Object from) {
        if (test.test(from)) {
            return Decorator.decorate((T) from, mappings, delegateKey != null ? delegateKey.toString() : null,
                    configuration);
        }
        return null;
    }

    public static class Builder<T> extends AbstractBuilder<T, Builder<T>> {

        private final Predicate<Object> test;

        private Builder(Predicate<Object> test) {
            super();
            Checker.checkArgumentNotNull(test);
            this.test = test;
        }

        @Override
        protected Builder<T> self() {
            return this;
        }

        /**
         *
         * @return a new decorator converter instance
         */
        public DecoratorConverter<T> build() {
            return new DecoratorConverter<T>(test, delegateKey, ImmutableMap.copyOf(mappings));
        }

    }

}


/*
 * Copyright 2015 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.engine.segment;

import java.io.IOException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.handlebars.Options;

/**
 * A wrapper for an asynchronous appendable. This construct is not thread-safe.
 *
 * @author Martin Kouba
 * @see Options#executeAsync(org.trimou.handlebars.Options.HelperExecutable)
 */
class AsyncAppendable implements Appendable {

    protected final Appendable parent;

    protected final StringBuilder buffer;

    protected volatile Future<AsyncAppendable> future;

    /**
     *
     * @param parent
     */
    AsyncAppendable(Appendable parent) {
        this.parent = parent;
        this.buffer = new StringBuilder();
    }

    @Override
    public Appendable append(CharSequence csq) throws IOException {
        buffer.append(csq);
        return this;
    }

    @Override
    public Appendable append(CharSequence csq, int start, int end)
            throws IOException {
        buffer.append(csq, start, end);
        return this;
    }

    @Override
    public Appendable append(char c) throws IOException {
        buffer.append(c);
        return this;
    }

    /**
     * Append the result to the parent.
     */
    private void flush() {
        try {
            parent.append(future.get(60, TimeUnit.SECONDS).collect(this));
            parent.append(buffer);
            if (parent instanceof AsyncAppendable) {
                ((AsyncAppendable) parent).flush();
            }
        } catch (Exception e) {
            throw new MustacheException(
                    MustacheProblem.RENDER_ASYNC_PROCESSING_ERROR, e);
        }
    }

    /**
     *
     * @param latch
     *            The top-level async appendable
     * @return the collected output
     */
    private CharSequence collect(AsyncAppendable latch) {

        if (parent.equals(latch) && future == null) {
            return buffer;
        }

        StringBuilder ret = null;

        if (parent instanceof AsyncAppendable && !parent.equals(latch)) {
            ret = new StringBuilder();
            ret.append(((AsyncAppendable) parent).collect(latch));
        }

        if (future != null) {
            try {
                if (ret == null) {
                    ret = new StringBuilder();
                }
                AsyncAppendable result = future.get(60, TimeUnit.SECONDS);
                if (result.future != null) {
                    ret.append(result.collect(this));
                } else {
                    ret.append(result.buffer);
                    ret.append(buffer);
                }
                return ret;
            } catch (Exception e) {
                throw new MustacheException(
                        MustacheProblem.RENDER_ASYNC_PROCESSING_ERROR, e);
            }
        } else {
            return ret != null ? ret.append(buffer) : buffer;
        }
    }

    void setFuture(Future<AsyncAppendable> future) {
        this.future = future;
    }

    static void flushIfNeeded(Appendable appendable) {
        if (appendable instanceof AsyncAppendable) {
            ((AsyncAppendable) appendable).flush();
        }
    }

}


/*
 * 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.engine.config;

/**
 * Abstract configuration-aware component that holds a reference to the
 * {@link Configuration} instance.
 *
 * @author Martin Kouba
 */
public abstract class AbstractConfigurationAware implements ConfigurationAware {

    protected volatile Configuration configuration;

    /**
     * When overriding this method, always call super.init(config).
     */
    @Override
    public void init(Configuration configuration) {
        checkNotInitialized(this.configuration != null);
        this.configuration = configuration;
        this.init();
    }

    /**
     * Can be overridden so that there's no need to call
     * super.init(Configuration).
     */
    protected void init() {
    }

    /**
     * @throws IllegalStateException
     *             If the isInitializedExpression evaluates to true
     */
    protected void checkNotInitialized(boolean isInitializedExpression) {
        if (isInitializedExpression) {
            throw new IllegalStateException(
                    "Component is already initialized!");
        }
    }

}


/*
 * 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.engine.resolver;

import static org.trimou.engine.priority.Priorities.after;
import static org.trimou.util.Checker.checkArgumentNotNull;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.cache.ComputingCache;
import org.trimou.engine.config.ConfigurationKey;
import org.trimou.engine.config.SimpleConfigurationKey;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.util.ImmutableSet;
import org.trimou.util.Reflections;

/**
 * Reflection-based resolver attempts to find a matching member on the context
 * object class and its superclasses. Methods have higher priority than fields.
 *
 * @author Martin Kouba
 * @see Reflections#findField(Class, String)
 * @see Reflections#findMethod(Class, String)
 */
public class ReflectionResolver extends AbstractResolver {

    public static final int REFLECTION_RESOLVER_PRIORITY = after(ListIndexResolver.LIST_RESOLVER_PRIORITY, 3);

    public static final String COMPUTING_CACHE_CONSUMER_ID = ReflectionResolver.class
            .getName();

    /**
     * Limit the size of the cache (e.g. to avoid problems when dynamic class
     * compilation is involved). Use zero value to disable the cache.
     */
    public static final ConfigurationKey MEMBER_CACHE_MAX_SIZE_KEY = new SimpleConfigurationKey(
            ReflectionResolver.class.getName() + ".memberCacheMaxSize", 10000L);

    /**
     * Even if the runtime class of the context object changes try to apply the
     * resolver.
     */
    public static final ConfigurationKey HINT_FALLBACK_ENABLED_KEY = new SimpleConfigurationKey(
            ReflectionResolver.class.getName() + ".hintFallbackEnabled", true);

    private static final Logger LOGGER = LoggerFactory
            .getLogger(ReflectionResolver.class);

    private static final MemberWrapper ARRAY_GET_LENGTH = Array::getLength;

    public static final String GET_PREFIX = "get";
    public static final String IS_PREFIX = "is";

    /**
     * Lazy loading cache of lookup attempts (contains both hits and misses)
     */
    private ComputingCache<MemberKey, Optional<MemberWrapper>> memberCache;

    private boolean hintFallbackEnabled;

    public ReflectionResolver() {
        this(REFLECTION_RESOLVER_PRIORITY);
    }

    public ReflectionResolver(int priority) {
        super(priority);
    }

    @Override
    public Object resolve(Object contextObject, String name,
            ResolutionContext context) {

        if (contextObject == null) {
            return null;
        }

        MemberWrapper wrapper;
        MemberKey key = MemberKey.newInstance(contextObject, name);
        if (memberCache != null) {
            wrapper = memberCache.get(key).orElse(null);
        } else {
            wrapper = findWrapper(key).orElse(null);
        }

        if (wrapper == null) {
            return null;
        }

        try {
            return wrapper.getValue(contextObject);
        } catch (Exception e) {
            throw new MustacheException(
                    MustacheProblem.RENDER_REFLECT_INVOCATION_ERROR, e);
        }
    }

    @Override
    public Hint createHint(Object contextObject, String name,
            ResolutionContext context) {
        MemberKey key = MemberKey.newInstance(contextObject, name);
        MemberWrapper wrapper;
        if (memberCache != null) {
            Optional<MemberWrapper> found = memberCache.getIfPresent(key);
            wrapper = found != null ? found.get() : null;
        } else {
            wrapper = findWrapper(key).orElse(null);
        }
        if (wrapper != null) {
            return new ReflectionHint(key, wrapper);
        }
        // This should never happen
        return Hints.INAPPLICABLE_HINT;
    }

    @Override
    public void init() {
        long memberCacheMaxSize = configuration
                .getLongPropertyValue(MEMBER_CACHE_MAX_SIZE_KEY);
        LOGGER.debug("Initialized [memberCacheMaxSize: {}]",
                memberCacheMaxSize);
        if (memberCacheMaxSize > 0) {
            memberCache = configuration.getComputingCacheFactory().create(
                    COMPUTING_CACHE_CONSUMER_ID, ReflectionResolver::findWrapper,
                    null, memberCacheMaxSize, null);
        }
        hintFallbackEnabled = configuration
                .getBooleanPropertyValue(HINT_FALLBACK_ENABLED_KEY);
    }

    @Override
    public Set<ConfigurationKey> getConfigurationKeys() {
        return ImmutableSet.of(MEMBER_CACHE_MAX_SIZE_KEY, HINT_FALLBACK_ENABLED_KEY);
    }

    /**
     * The member cache may theoretically cause memory leaks due to using hard
     * references to {@link Class} and {@link Member} instances. As a temporary
     * workaround we provide a way to invalidate the cache or some of its
     * entries (e.g. for a concrete classloader).
     *
     * @param predicate
     *            If null, all cache entries are discarded, otherwise an entry
     *            is only discarded if the given predicate returns
     *            true for the {@link MemberKey#getClass()}
     */
    public void invalidateMemberCache(Predicate<Class<?>> predicate) {
        if (memberCache == null) {
            return;
        }
        if (predicate == null) {
            memberCache.clear();
        } else {
            memberCache.invalidate((key) -> predicate.test(key.getClazz()));
        }
    }

    long getMemberCacheSize() {
        return memberCache != null ? memberCache.size() : 0L;
    }

    private static Optional<MemberWrapper> findWrapper(MemberKey key) {
        // Get length of array objects
        if (key.getClazz().isArray()) {
            if (key.getName().equals("length")) {
                return Optional.of(ARRAY_GET_LENGTH);
            } else {
                return Optional.empty();
            }
        }

        // Find accesible method with the given name, no
        // parameters and non-void return type
        Method foundMethod = findMethod(key.getClazz(), key.getName());

        if (foundMethod != null) {
            if (!foundMethod.isAccessible()) {
                SecurityActions.setAccessible(foundMethod);
            }
            return Optional.of(new MethodWrapper(foundMethod));
        }

        // Find public field
        Field foundField = findField(key.getClazz(), key.getName());

        if (foundField != null) {
            if (!foundField.isAccessible()) {
                SecurityActions.setAccessible(foundField);
            }
            return Optional.of(new FieldWrapper(foundField));
        }
        // Member not found
        return Optional.empty();
    }

    private class ReflectionHint implements Hint {

        private final MemberKey key;

        private final MemberWrapper wrapper;

        /**
         *
         * @param key
         * @param wrapper
         */
        ReflectionHint(MemberKey key, MemberWrapper wrapper) {
            this.key = key;
            this.wrapper = wrapper;
        }

        @Override
        public Object resolve(Object contextObject, String name,
                ResolutionContext context) {
            if (contextObject == null) {
                return null;
            }
            if (key.getClazz().equals(contextObject.getClass())) {
                try {
                    return wrapper.getValue(contextObject);
                } catch (Exception e) {
                    return null;
                }
            }
            // The runtime class of the context object changed
            if (ReflectionResolver.this.hintFallbackEnabled) {
                return ReflectionResolver.this.resolve(contextObject, name,
                        context);
            }
            return null;
        }
    }

    /**
     * First tries to find a valid method with the same name, afterwards method
     * following JavaBean naming convention (the method starts with
     * get/is prefix).
     *
     * @param clazz
     * @param name
     * @return the found method or null
     */
    static Method findMethod(Class<?> clazz, String name) {

        checkArgumentNotNull(clazz);
        checkArgumentNotNull(name);

        Method foundMatch = null;
        Method foundGetMatch = null;
        Method foundIsMatch = null;

        for (Method method : SecurityActions.getMethods(clazz)) {

            if (!isMethodValid(method)) {
                continue;
            }

            if (method.isBridge()) {
                LOGGER.debug("Skipping bridge method {}", method);
                continue;
            }

            if (name.equals(method.getName())) {
                foundMatch = method;
            } else if (Reflections.matchesPrefix(name, method.getName(),
                    GET_PREFIX)) {
                foundGetMatch = method;
            } else if (Reflections.matchesPrefix(name, method.getName(),
                    IS_PREFIX)) {
                foundIsMatch = method;
            }
        }

        if (foundMatch == null) {
            foundMatch = (foundGetMatch != null ? foundGetMatch : foundIsMatch);
        }

        LOGGER.debug("Method for [{}] {}found on type {}: {}", name, foundMatch != null ? "" : "not ", clazz.getName(),
                foundMatch);
        return foundMatch;
    }

    /**
     * Tries to find a public field with the given name on the given class.
     *
     * @param clazz
     * @param name
     * @return the found field or null
     */
    static Field findField(Class<?> clazz, String name) {

        checkArgumentNotNull(clazz);
        checkArgumentNotNull(name);

        Field found = null;

        for (Field field : SecurityActions.getFields(clazz)) {
            if (field.getName().equals(name)) {
                found = field;
            }
        }
        LOGGER.debug("Field {} {}found on type: {}", name, found != null ? "" : "not ", clazz.getName());
        return found;
    }

    /**
     * A valid method:
     *

     *
is public

     *
has no parameters

     *
has non-void return type

     *
its declaring class is not {@link Object}

     *

     *
     * @param method
     * @return true if the given method is considered a read method
     */
    private static boolean isMethodValid(Method method) {
        return method != null && Modifier.isPublic(method.getModifiers())
                && method.getParameterTypes().length == 0
                && !method.getReturnType().equals(Void.TYPE)
                && !Object.class.equals(method.getDeclaringClass());
    }

}


/*
 * Copyright 2016 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.engine.config;

import java.util.Map;
import java.util.Map.Entry;

import org.trimou.engine.config.ConfigurationExtension.ConfigurationExtensionBuilder;
import org.trimou.handlebars.Helper;
import org.trimou.util.Checker;

/**
 *
 * @author Martin Kouba
 */
public class ConfigurationExtensions {

    private ConfigurationExtensions() {
    }

    /**
     * Register helper and do nothing if a helper with the same name is already registered.
     *
     * @param builder
     * @param helpers
     */
    public static void registerHelpers(ConfigurationExtensionBuilder builder, Map<String, Helper> helpers) {
        Checker.checkArgumentsNotNull(builder, helpers);
        for (Entry<String, Helper> entry : helpers.entrySet()) {
            registerHelper(builder, entry.getKey(), entry.getValue());
        }
    }

    /**
     * Register helpers and do nothing if a helper with the same name is already registered.
     *
     * @param builder
     * @param name
     * @param helper
     */
    public static void registerHelper(ConfigurationExtensionBuilder builder, String name, Helper helper) {
        Checker.checkArgumentsNotNull(builder, name, helper);
        try {
            builder.registerHelper(name, helper);
        } catch (IllegalArgumentException ignored) {
        }
    }

}


/*
 * 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.engine.segment;

import static org.trimou.engine.config.EngineConfigurationKey.DEBUG_MODE;
import static org.trimou.engine.config.EngineConfigurationKey.TEMPLATE_CACHE_ENABLED;
import static org.trimou.engine.config.EngineConfigurationKey.TEMPLATE_CACHE_EXPIRATION_TIMEOUT;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.trimou.engine.MustacheEngine;
import org.trimou.engine.config.Configuration;
import org.trimou.engine.parser.Template;

/**
 * {@link Segment} utils.
 *
 * @author Martin Kouba
 */
final class Segments {

    private Segments() {
    }

    /**
     *
     * @param configuration
     * @return true if it's possible to cache the template in a
     *         segment, i.e. if the cache is enabled, no expiration timeout is
     *         set and debug mode is not enabled, false otherwise
     */
    static boolean isTemplateCachingAllowed(Configuration configuration) {
        return !configuration.getBooleanPropertyValue(DEBUG_MODE)
                && configuration.getBooleanPropertyValue(TEMPLATE_CACHE_ENABLED)
                && configuration.getLongPropertyValue(
                        TEMPLATE_CACHE_EXPIRATION_TIMEOUT) <= 0;
    }

    /**
     *
     * @param cachedReference
     * @param templateId
     * @param engine
     * @return the template, use the cache if possible
     */
    static Template getTemplate(AtomicReference<Template> cachedReference,
            String templateId, MustacheEngine engine) {
        return getTemplate(cachedReference, templateId, engine, null);
    }

    /**
     *
     * @param cachedReference
     * @param templateId
     * @param engine
     * @param currentTemplate If set try to lookup nested templates first
     * @return the template, use the cache if possible
     */
    static Template getTemplate(AtomicReference<Template> cachedReference,
            String templateId, MustacheEngine engine, Template currentTemplate) {
        if (cachedReference != null) {
            Template template = cachedReference.get();
            if (template == null) {
                synchronized (cachedReference) {
                    if (template == null) {
                        template = lookupTemplate(templateId, engine, currentTemplate);
                        cachedReference.set(template);
                    }
                }
            }
            return template;
        }
        return lookupTemplate(templateId, engine, currentTemplate);
    }

    /**
     *
     * @param templateId
     * @param engine
     * @param currentTemplate
     * @return the template or null
     */
    public static Template lookupTemplate(String templateId, MustacheEngine engine,
            Template currentTemplate) {
        Template result = null;
        if (currentTemplate != null) {
            result = currentTemplate.getNestedTemplate(templateId);
        }
        if (result == null) {
            result = (Template) engine.getMustache(templateId);
        }
        return result;
    }

    /**
     * Read segment lines before rendering.
     *
     * @param container
     * @return
     */
    static List<List<Segment>> readSegmentLinesBeforeRendering(
            AbstractContainerSegment container) {
        List<List<Segment>> lines = new ArrayList<>();
        List<Segment> currentLine = new ArrayList<>();

        for (Segment segment : container) {
            if (!SegmentType.LINE_SEPARATOR.equals(segment.getType())) {
                currentLine.add(segment);
            } else {
                // New line separator - flush the line
                currentLine.add(segment);
                lines.add(currentLine);
                currentLine = new ArrayList<>();
            }
        }
        // Add the last line manually - there is no line separator to trigger
        // flush
        if (!currentLine.isEmpty()) {
            lines.add(currentLine);
        }
        return lines;
    }

}


/*
 * Copyright 2016 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.EXPIRE;
import static org.trimou.handlebars.OptionsHashKeys.GUARD;
import static org.trimou.handlebars.OptionsHashKeys.KEY;
import static org.trimou.handlebars.OptionsHashKeys.UNIT;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.trimou.engine.cache.ComputingCache;
import org.trimou.engine.config.ConfigurationKey;
import org.trimou.engine.config.SimpleConfigurationKey;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;
import org.trimou.handlebars.HelperDefinition.ValuePlaceholder;
import org.trimou.util.ImmutableSet;

/**
 * Allows to cache template fragments in memory. This might be useful for
 * resource-intensive parts of the template that rarely change (e.g. a user menu
 * in a webapp).
 *
 *
 * {{#cache}}
 *   All the content will be cached!
 * {{/cache}}
 *
 *
 *

 * A key might be used to cache multiple versions of the fragment
 * for the same part of the template, i.e. each version depends on some context
 * (e.g. logged-in user):
 *

 *
 *
 * {{#cache key=username}}
 *   All the content will be cached!
 * {{/cache}}
 *
 *
 *

 * The cached fragments can be automatically updated. Either set the expiration
 * or the guard. expire value must be a Long value.
 * unit must be a {@link TimeUnit} constant or its
 * {@link Object#toString()} ( {@link TimeUnit#MILLISECONDS} is the default).
 * guard may be any object - every time the helper is executed the
 * current value of the {@link Object#toString()} is compared to the
 * {@link Object#toString()} value of the guard referenced during the last
 * update of the fragment. If they're not equal the fragment is updated.
 *

 *
 *
 * {{#cache key=username expire=2 unit="HOURS" guard=roles}}
 *   All the content will be cached!
 * {{/cache}}
 *
 *
 *

 * It's also possible to invalidate the cache manually (e.g. after user logout).
 * See also {@link #invalidateFragments()} and
 * {@link #invalidateFragments(String)}.
 *

 *
 *

 * To limit the size of the fragment cache use
 * {@link #FRAGMENT_CACHE_MAX_SIZE_KEY}.
 *

 *
 * @author Martin Kouba
 */
public class CacheHelper extends BasicSectionHelper {

    /**
     * Limit the size of the fragment cache.
     */
    public static final ConfigurationKey FRAGMENT_CACHE_MAX_SIZE_KEY = new SimpleConfigurationKey(
            CacheHelper.class.getName() + ".fragmentCacheMaxSize", 500L);

    private static final Logger LOGGER = LoggerFactory
            .getLogger(CacheHelper.class);

    private volatile ComputingCache<Key, Fragment> fragments;

    @Override
    public void execute(Options options) {

        Map<String, Object> hash = options.getHash();
        Object key = hash.get(KEY);
        Object guard = hash.get(GUARD);
        Object expire = hash.get(EXPIRE);
        Object unit = expire != null ? hash.get(UNIT) : null;

        StringBuilder fragmentKey = new StringBuilder();
        fragmentKey.append(options.getTagInfo().getTemplateGeneratedId());
        fragmentKey.append(options.getTagInfo().getId());
        if (key != null) {
            fragmentKey.append(key.toString());
        }

        Fragment fragment = fragments
                .get(new Key(fragmentKey.toString(), options));

        if (fragment.getHits() > 0 && (isExpired(fragment, expire, unit)
                || isGuardCompromised(fragment, guard))) {
            fragment.update(getContent(options), guard);
        }
        fragment.touch();
        options.append(fragment.getContent());
    }

    public void init() {
        super.init();
        this.fragments = configuration.getComputingCacheFactory().create(
                CacheHelper.class.getName(),
                key -> {
                    Fragment fragment = new Fragment();
                    fragment.update(getContent(key.getOptions()),
                            key.getOptions().getHash().get(GUARD));
                    key.cleanupAfterCompute();
                    return fragment;
                }, null,
                configuration.getLongPropertyValue(FRAGMENT_CACHE_MAX_SIZE_KEY),
                null);
    }

    @Override
    public Set<ConfigurationKey> getConfigurationKeys() {
        return Collections.singleton(FRAGMENT_CACHE_MAX_SIZE_KEY);
    }

    @Override
    protected int numberOfRequiredParameters() {
        return 0;
    }

    @Override
    public void validate(HelperDefinition definition) {
        super.validate(definition);
        Map<String, Object> hash = definition.getHash();
        if (hash.containsKey(EXPIRE)) {
            Object expire = hash.get(EXPIRE);
            if (!(expire instanceof ValuePlaceholder)) {
                try {
                    Long.valueOf(expire.toString());
                } catch (NumberFormatException e) {
                    throw new MustacheException(
                            MustacheProblem.COMPILE_HELPER_VALIDATION_FAILURE,
                            e);
                }
            }
            Object unit = hash.get(UNIT);
            if (unit != null && !(unit instanceof TimeUnit)
                    && !(unit instanceof ValuePlaceholder)) {
                try {
                    TimeUnit.valueOf(unit.toString());
                } catch (Exception e) {
                    throw new MustacheException(
                            MustacheProblem.COMPILE_HELPER_VALIDATION_FAILURE,
                            e);
                }
            }
        }
        if (!hash.containsKey(EXPIRE) && !hash.containsKey(GUARD)) {
            LOGGER.info(
                    "Cache fragment will not be automatically updated [helper: {}, template: {}, line: {}]",
                    getClass().getName(),
                    definition.getTagInfo().getTemplateName(),
                    definition.getTagInfo().getLine());
        }
    }

    /**
     * Invalidate all the cache fragments.
     */
    public void invalidateFragments() {
        if (fragments == null) {
            return;
        }
        fragments.clear();
    }

    /**
     * Invalidate the cache fragments whose key contains the given part of the
     * key.
     *
     * @param keyPart
     */
    public void invalidateFragments(final String keyPart) {
        if (fragments == null || keyPart == null) {
            return;
        }
        fragments.invalidate(fragmentKey -> fragmentKey.getKey().contains(keyPart));
    }

    @Override
    protected Set<String> getSupportedHashKeys() {
        return ImmutableSet.of(KEY, GUARD, EXPIRE, UNIT);
    }

    private boolean isExpired(Fragment fragment, Object expire, Object unit) {
        if (expire == null) {
            return false;
        }
        TimeUnit timeUnit;
        if (unit != null) {
            if (unit instanceof TimeUnit) {
                timeUnit = (TimeUnit) unit;
            } else {
                try {
                    timeUnit = TimeUnit.valueOf(unit.toString());
                } catch (Exception e) {
                    throw new MustacheException(
                            MustacheProblem.RENDER_HELPER_INVALID_OPTIONS, e);
                }
            }
        } else {
            timeUnit = TimeUnit.MILLISECONDS;
        }
        Long duration;
        if (expire instanceof Long) {
            duration = (Long) expire;
        } else if (expire instanceof Integer) {
            duration = ((Integer) expire).longValue();
        } else {
            try {
                duration = Long.valueOf(expire.toString());
            } catch (Exception e) {
                throw new MustacheException(
                        MustacheProblem.RENDER_HELPER_INVALID_OPTIONS, e);
            }
        }
        return (System.currentTimeMillis() - fragment.getLastUsed()) > timeUnit
                .toMillis(duration);
    }

    private boolean isGuardCompromised(Fragment fragment, Object guard) {
        if (guard == null) {
            return false;
        }
        return !guard.toString().equals(fragment.getGuard());
    }

    private static String getContent(Options options) {
        StringBuilder content = new StringBuilder();
        options.fn(content);
        return content.toString();
    }

    private static class Fragment {

        private final AtomicLong hits;

        private final AtomicLong lastUsed;

        private final AtomicReference<String> content;

        private final AtomicReference<String> guard;

        private Fragment() {
            this.hits = new AtomicLong(0);
            this.lastUsed = new AtomicLong();
            this.content = new AtomicReference<>();
            this.guard = new AtomicReference<>();
        }

        /**
         * @return the cached content
         */
        String getContent() {
            return content.get();
        }

        void touch() {
            this.hits.incrementAndGet();
            this.lastUsed.set(System.currentTimeMillis());
        }

        void update(String content, Object guard) {
            this.content.set(content);
            if (guard != null) {
                this.guard.set(guard.toString());
            }
        }

        /**
         * @return the lastHit
         */
        Long getLastUsed() {
            return lastUsed.get();
        }

        /**
         * @return the number of hits
         */
        Long getHits() {
            return hits.get();
        }

        /**
         * @return the guard
         */
        String getGuard() {
            return guard.get();
        }

    }

    private static class Key {

        private final String key;

        private Options options;

        private Key(String key, Options options) {
            this.key = key;
            this.options = options;
        }

        void cleanupAfterCompute() {
            this.options = null;
        }

        /**
         * @return the key
         */
        String getKey() {
            return key;
        }

        /**
         * @return the options
         */
        Options getOptions() {
            return options;
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((key == null) ? 0 : key.hashCode());
            return result;
        }

        /*
         * (non-Javadoc)
         *
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            Key other = (Key) obj;
            if (key == null) {
                if (other.key != null) {
                    return false;
                }
            } else if (!key.equals(other.key)) {
                return false;
            }
            return true;
        }

    }

}


/*
 * Copyright 2015 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.engine.context;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.trimou.engine.config.Configuration;
import org.trimou.engine.config.EngineConfigurationKey;
import org.trimou.engine.convert.ContextConverter;
import org.trimou.engine.parser.Template;
import org.trimou.engine.resolver.EnhancedResolver;
import org.trimou.engine.resolver.EnhancedResolver.Hint;
import org.trimou.engine.resolver.Placeholder;
import org.trimou.engine.resolver.Resolver;
import org.trimou.engine.segment.Segment;
import org.trimou.exception.MustacheException;
import org.trimou.exception.MustacheProblem;

/**
 * A default implementation.
 *
 * @author Martin Kouba
 */
final class DefaultExecutionContext implements ExecutionContext {

    private final DefaultExecutionContext parent;

    private final Configuration configuration;

    protected final Object contextObject;

    protected final Template templateInvocation;

    protected final int invocationLimitCounter;

    protected final Map<String, Segment> definingSections;

    protected final Resolver[] resolvers;

    private final List<ContextConverter> converters;

    /**
     *
     * @param parent
     * @param configuration
     * @param contextObject
     * @param templateInvocation
     * @param templateInvocations
     * @param invocationLimitCounter
     * @param definingSections
     * @param resolvers
     * @param converters
     */
    DefaultExecutionContext(DefaultExecutionContext parent,
            Configuration configuration, Object contextObject,
            Template templateInvocation, int invocationLimitCounter,
            Map<String, Segment> definingSections, Resolver[] resolvers, List<ContextConverter> converters) {
        this.parent = parent;
        this.configuration = configuration;
        this.contextObject = contextObject;
        this.templateInvocation = templateInvocation;
        this.invocationLimitCounter = invocationLimitCounter;
        this.definingSections = definingSections;
        this.resolvers = resolvers;
        this.converters = converters;
    }

    @Override
    public ValueWrapper getValue(String key, String[] keyParts,
            AtomicReference<Hint> hintRef) {

        ValueWrapper value = new ValueWrapper(key);
        Object lastValue;

        if (keyParts == null || keyParts.length == 0) {
            Iterator<String> parts = configuration.getKeySplitter().split(key);
            lastValue = resolveLeadingContextObject(parts.next(), value,
                    hintRef);
            if (lastValue == null) {
                // Leading context object not found - miss
                return value;
            }
            while (parts.hasNext()) {
                value.processNextPart();
                lastValue = resolve(lastValue, parts.next(), value, false);
                if (lastValue == null) {
                    // Not found - miss
                    return value;
                }
            }
        } else {
            lastValue = resolveLeadingContextObject(keyParts[0], value, hintRef);
            if (lastValue == null) {
                // Leading context object not found - miss
                return value;
            }
            if (keyParts.length > 1) {
                for (int i = 1; i < keyParts.length; i++) {
                    value.processNextPart();
                    lastValue = resolve(lastValue, keyParts[i], value, false);
                    if (lastValue == null) {
                        // Not found - miss
                        return value;
                    }
                }
            }
        }

        if (!Placeholder.NULL.equals(lastValue)) {
            value.set(lastValue);
        }
        return value;
    }

    @Override
    public ValueWrapper getValue(String key) {
        return getValue(key, null, null);
    }

    @Override
    public ExecutionContext setContextObject(Object object) {
        if (converters != null) {
            for (ContextConverter converter : converters) {
                Object result = converter.convert(object);
                if (result != null) {
                    object = result;
                    break;
                }
            }
        }
        return new DefaultExecutionContext(this, configuration, object, null,
                invocationLimitCounter, null, resolvers, converters);
    }

    @Override
    public Object getFirstContextObject() {
        if (contextObject != null) {
            return contextObject;
        }
        if (parent != null) {
            return parent.getFirstContextObject();
        }
        return null;
    }

    @Override
    public ExecutionContext setTemplateInvocation(Template template) {
        if (invocationLimitCounter < 0
                && getTemplateInvocations(template) > configuration
                        .getIntegerPropertyValue(EngineConfigurationKey.TEMPLATE_RECURSIVE_INVOCATION_LIMIT)) {
            throw new MustacheException(
                    MustacheProblem.RENDER_TEMPLATE_INVOCATION_RECURSIVE_LIMIT_EXCEEDED,
                    "Recursive invocation limit exceeded [limit: %s, level: %s, template: %s]",
                    configuration
                            .getIntegerPropertyValue(EngineConfigurationKey.TEMPLATE_RECURSIVE_INVOCATION_LIMIT),
                    invocationLimitCounter, templateInvocation);
        }
        return new DefaultExecutionContext(this, configuration, null, template,
                invocationLimitCounter - 1, null, resolvers, converters);
    }

    @Override
    public ExecutionContext setDefiningSections(Iterable<Segment> segments) {
        Map<String, Segment> definingSections = null;
        for (Segment segment : segments) {
            if (getDefiningSection(segment.getText()) == null) {
                if (definingSections == null) {
                    definingSections = new HashMap<>();
                }
                definingSections.put(segment.getText(), segment);
            }
        }
        return new DefaultExecutionContext(this, configuration, null, null,
                invocationLimitCounter, definingSections, resolvers, converters);
    }

    @Override
    public Segment getDefiningSection(String name) {
        Segment section = null;
        if (definingSections != null) {
            section = definingSections.get(name);
        }
        if (section == null && parent != null) {
            section = parent.getDefiningSection(name);
        }
        return section;
    }

    @Override
    public ExecutionContext getParent() {
        return parent;
    }

    private int getTemplateInvocations(Template template) {
        int invocations = 0;
        if (templateInvocation != null && templateInvocation.equals(template)) {
            invocations++;
        }
        if (parent != null) {
            invocations += parent.getTemplateInvocations(template);
        }
        return invocations;
    }

    /**
     * Resolve the leading context object (the first part of the key). E.g.
     * foo in {{foo.bar.name}} may identify a property
     * of some context object on the stack (passed data, section iteration,
     * nested context, ...), or some context and data unrelated object (e.g. CDI
     * bean).
     *
     * @param name
     * @param value
     *            The value wrapper - ResolutionContext
     * @param hintRef
     * @return the resolved leading context object
     * @see Hint
     */
    private Object resolveLeadingContextObject(String name, ValueWrapper value,
            AtomicReference<Hint> hintRef) {

        Object leading = resolveContextObject(name, value, hintRef);

        if (leading == null) {
            // Leading context object not found - try to resolve context
            // unrelated objects (JNDI lookup, CDI, etc.)
            Hint hint = hintRef != null ? hintRef.get() : null;
            if (hint != null) {
                leading = hint.resolve(null, name, value);
            }
            if (leading == null) {
                leading = resolve(null, name, value, hint == null
                        && hintRef != null);
            }
        }
        return leading;
    }

    private Object resolveContextObject(String name, ValueWrapper value,
            AtomicReference<Hint> hintRef) {

        Object leading = null;

        if (contextObject != null) {
            Hint hint = hintRef != null ? hintRef.get() : null;
            if (hint != null) {
                leading = hint.resolve(contextObject, name, value);
            }
            if (leading == null) {
                leading = resolve(contextObject, name, value, hint == null
                        && hintRef != null);
            }
        }
        if (leading == null && parent != null) {
            leading = parent.resolveContextObject(name, value, hintRef);
        }
        return leading;
    }

    private Object resolve(Object contextObject, String name, ValueWrapper value, boolean createHint) {
        Object resolved = null;
        for (final Resolver resolver : resolvers) {
            resolved = resolver.resolve(contextObject, name, value);
            if (resolved != null) {
                if (createHint && resolver instanceof EnhancedResolver) {
                    // Initialize a new hint if possible
                    value.setHint(((EnhancedResolver) resolver).createHint(contextObject, name, value));
                }
                break;
            }
        }
        return resolved;
    }

}


/*
 * Copyright 2014 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.i18n;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import org.trimou.engine.MustacheTagInfo;
import org.trimou.engine.cache.ComputingCache;

/**
 * An alternative to {@link DateTimeFormatHelper} which makes use of java.time
 * package in JDK 8 (JSR-310). It supports new temporal types and should also be
 * less resource-intensive.
 *
 * @author Martin Kouba
 */
public class TimeFormatHelper extends
        AbstractTimeFormatHelper<TemporalAccessor, FormatStyle> {

    private ComputingCache<CacheKey, DateTimeFormatter> formatterCache;

    protected void init() {
        super.init();
        formatterCache = configuration.getComputingCacheFactory().create(
                "todo",
                key -> {
                    DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
                    if (key.getPattern() != null) {
                        builder.appendPattern(key.getPattern());
                    } else if (key.getStyle() != null) {
                        builder.appendLocalized(key.getStyle(),
                                key.getStyle());
                    }
                    return builder.toFormatter(key.getLocale()).withZone(
                            key.getTimeZone().toZoneId());
                }, null, null, null);
    }

    protected String defaultFormat(TemporalAccessor value, Locale locale,
            TimeZone timeZone) {
        return format(value, FormatStyle.MEDIUM, locale, timeZone);
    }

    @Override
    protected String format(TemporalAccessor value, FormatStyle style,
            Locale locale, TimeZone timeZone) {
        return formatterCache.get(new CacheKey(locale, timeZone, null, style))
                .format(value);
    }

    @Override
    protected String format(TemporalAccessor value, String pattern,
            Locale locale, TimeZone timeZone) {
        return formatterCache
                .get(new CacheKey(locale, timeZone, pattern, null)).format(
                        value);
    }

    protected FormatStyle parseStyle(String style, MustacheTagInfo tagInfo) {
        try {
            return FormatStyle.valueOf(style.toUpperCase());
        } catch (Exception e) {
            throw unknownStyle(style, tagInfo);
        }
    }

    // TemporalAccessor should not be widely used in application code, but this
    // should be safe
    protected TemporalAccessor getFormattableObject(Object value,
            Locale locale, TimeZone timeZone, MustacheTagInfo tagInfo) {

        if (value instanceof TemporalAccessor) {
            return (TemporalAccessor) value;
        } else if (value instanceof Date) {
            return LocalDateTime.ofInstant(((Date) value).toInstant(),
                    timeZone.toZoneId());
        } else if (value instanceof Calendar) {
            return LocalDateTime.ofInstant(((Calendar) value).toInstant(),
                    timeZone.toZoneId());
        } else if (value instanceof Number) {
            return LocalDateTime.ofInstant(
                    Instant.ofEpochMilli(((Number) value).longValue()),
                    timeZone.toZoneId());
        } else {
            throw valueNotAFormattableObject(value, tagInfo);
        }
    }

    private static final class CacheKey {

        private final Locale locale;

        private final TimeZone timeZone;

        private final String pattern;

        private final FormatStyle style;

        public CacheKey(Locale locale, TimeZone timeZone, String pattern,
                FormatStyle style) {
            this.locale = locale;
            this.timeZone = timeZone;
            this.pattern = pattern;
            this.style = style;
        }

        protected Locale getLocale() {
            return locale;
        }

        protected TimeZone getTimeZone() {
            return timeZone;
        }

        protected String getPattern() {
            return pattern;
        }

        protected FormatStyle getStyle() {
            return style;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result
                    + ((locale == null) ? 0 : locale.hashCode());
            result = prime * result
                    + ((pattern == null) ? 0 : pattern.hashCode());
            result = prime * result + ((style == null) ? 0 : style.hashCode());
            result = prime * result
                    + ((timeZone == null) ? 0 : timeZone.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            CacheKey other = (CacheKey) obj;
            if (locale == null) {
                if (other.locale != null)
                    return false;
            } else if (!locale.equals(other.locale))
                return false;
            if (timeZone == null) {
                if (other.timeZone != null)
                    return false;
            } else if (!timeZone.equals(other.timeZone))
                return false;
            if (pattern == null) {
                if (other.pattern != null)
                    return false;
            } else if (!pattern.equals(other.pattern))
                return false;
            if (style != other.style)
                return false;
            return true;
        }

    }

}