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

org.thymeleaf.standard.expression.FragmentExpression Maven / Gradle / Ivy

Go to download

Modern server-side Java template engine for both web and standalone environments

There is a newer version: 3.1.2.RELEASE
Show newest version
/*
 * =============================================================================
 * 
 *   Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org)
 * 
 *   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.thymeleaf.standard.expression;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.TemplateModel;
import org.thymeleaf.exceptions.TemplateInputException;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.util.StringUtils;
import org.thymeleaf.util.Validate;


/**
 * 
 * @author Daniel Fernández
 * 
 * @since 3.0.0
 *
 */
public final class FragmentExpression extends SimpleExpression {

    private static final Logger logger = LoggerFactory.getLogger(FragmentExpression.class);

    private static final long serialVersionUID = -130371297698708001L;

    /**
     * This constant contains the {@link FragmentExpression} object representing the EMPTY FRAGMENT (~{})
     */
    public static final FragmentExpression EMPTY_FRAGMENT_EXPRESSION = new FragmentExpression();

    private static final String TEMPLATE_NAME_CURRENT_TEMPLATE = "this";
    private static final String SEPARATOR = "::";
    static final String UNNAMED_PARAMETERS_PREFIX = "_arg";


    public static final char SELECTOR = '~';


    private static final Pattern FRAGMENT_PATTERN =
        Pattern.compile("^\\s*~\\{(.*?)\\}\\s*$", Pattern.DOTALL);




    private final IStandardExpression templateName;
    private final IStandardExpression fragmentSelector;
    private final AssignationSequence parameters;
    private final boolean syntheticParameters;



    public FragmentExpression(
            final IStandardExpression templateName, final IStandardExpression fragmentSelector,
            final AssignationSequence parameters, final boolean syntheticParameters) {
        super();
        // templateName can be null if fragment is to be executed on the current template, but then there should
        // at least be a fragment selector
        if (templateName == null && fragmentSelector == null) {
            throw new IllegalArgumentException(
                    "Fragment Expression cannot have null template name and null fragment selector");
        }
        this.templateName = templateName;
        this.fragmentSelector = fragmentSelector;
        this.parameters = parameters;
        this.syntheticParameters = (this.parameters != null && this.parameters.size() > 0 && syntheticParameters);
    }


    // Creates the empty Fragment Expression
    private FragmentExpression() {
        super();
        this.templateName = null;
        this.fragmentSelector = null;
        this.parameters = null;
        this.syntheticParameters = false;
    }


    public IStandardExpression getTemplateName() {
        return this.templateName;
    }

    public IStandardExpression getFragmentSelector() {
        return this.fragmentSelector;
    }

    public boolean hasFragmentSelector() {
        return this.fragmentSelector != null;
    }

    public AssignationSequence getParameters() {
        return this.parameters;
    }

    public boolean hasParameters() {
        return this.parameters != null && this.parameters.size() > 0;
    }

    public boolean hasSyntheticParameters() {
        return this.syntheticParameters;
    }



    @Override
    public String getStringRepresentation() {

        final StringBuilder sb = new StringBuilder();

        sb.append(SELECTOR);
        sb.append(SimpleExpression.EXPRESSION_START_CHAR);

        sb.append(this.templateName != null? this.templateName.getStringRepresentation() : "");

        if (this.fragmentSelector != null) {
            sb.append(' ');
            sb.append(SEPARATOR);
            sb.append(' ');
            sb.append(this.fragmentSelector.getStringRepresentation());
        }

        if (this.parameters != null && this.parameters.size() > 0) {
            sb.append(' ');
            sb.append('(');
            sb.append(StringUtils.join(this.parameters.getAssignations(), ','));
            sb.append(')');
        }

        sb.append(SimpleExpression.EXPRESSION_END_CHAR);

        return sb.toString();

    }





    public static FragmentExpression parseFragmentExpression(final String input) {
        final Matcher matcher = FRAGMENT_PATTERN.matcher(input);
        if (!matcher.matches()) {
            return null;
        }
        final String expression = matcher.group(1);
        if (StringUtils.isEmptyOrWhitespace(expression)) {
            return EMPTY_FRAGMENT_EXPRESSION;
        }
        return parseFragmentExpressionContent(expression.trim());
    }




    static FragmentExpression parseFragmentExpressionContent(final String input) {

        if (StringUtils.isEmptyOrWhitespace(input)) {
            return EMPTY_FRAGMENT_EXPRESSION;
        }

        final String trimmedInput = input.trim();

        final int lastParenthesesGroupPos = indexOfLastParenthesesGroup(trimmedInput);

        final String inputWithoutParameters;
        String parametersStr;
        if (lastParenthesesGroupPos != -1) {
            parametersStr = trimmedInput.substring(lastParenthesesGroupPos).trim();
            inputWithoutParameters = trimmedInput.substring(0, lastParenthesesGroupPos).trim();
        } else {
            parametersStr = null;
            inputWithoutParameters = trimmedInput;
        }


        String templateNameStr;
        String fragmentSpecStr;
        final int operatorPos = inputWithoutParameters.indexOf(SEPARATOR);
        if (operatorPos == -1) {
            // no operator means everything is considered "before operator" (there is template name, but no
            // fragment name -- template is to be included in its entirety).

            templateNameStr = inputWithoutParameters;
            fragmentSpecStr = null;
            if (StringUtils.isEmptyOrWhitespace(templateNameStr)) {
                if (parametersStr != null) {
                    // Parameters weren't parameters, they actually were the template name!
                    templateNameStr = parametersStr;
                    parametersStr = null;
                } else {
                    // parameters are null, so template name is empty, and therefore wrong.
                    return null;
                }
            }

        } else {
            // There IS operator: we should divide between template name (which can be empty) and fragment spec.

            templateNameStr = inputWithoutParameters.substring(0, operatorPos).trim();
            fragmentSpecStr = inputWithoutParameters.substring(operatorPos + SEPARATOR.length()).trim();
            if (StringUtils.isEmptyOrWhitespace(fragmentSpecStr)) {
                if (parametersStr != null) {
                    // Parameters weren't parameters, they actually were the fragment spec!
                    fragmentSpecStr = parametersStr;
                    parametersStr = null;
                } else {
                    // parameters are null, so fragment specification is empty, and therefore wrong (because we
                    // have already established that the :: operator IS present.
                    return null;
                }
            }

        }

        final Expression templateNameExpression;
        if (!StringUtils.isEmptyOrWhitespace(templateNameStr)) {
            templateNameExpression = parseDefaultAsLiteral(templateNameStr);
            if (templateNameExpression == null) {
                return null;
            }
        } else {
            templateNameExpression = null;
        }

        final Expression fragmentSpecExpression;
        if (!StringUtils.isEmptyOrWhitespace(fragmentSpecStr)) {
            fragmentSpecExpression = parseDefaultAsLiteral(fragmentSpecStr);
            if (fragmentSpecExpression == null) {
                return null;
            }
        } else {
            fragmentSpecExpression = null;
        }

        if (!StringUtils.isEmptyOrWhitespace(parametersStr)) {

            // When parsing this, we don't allow parameters without value because we would be mistakenly
            // parsing as parameter names what in fact are values for synthetically named parameters.
            final AssignationSequence parametersAsSeq =
                    AssignationUtils.internalParseAssignationSequence(parametersStr, false);

            if (parametersAsSeq != null) {
                return new FragmentExpression(templateNameExpression, fragmentSpecExpression, parametersAsSeq, false);
            }

            // Parameters wheren't parsable as an assignation sequence. So we should try parsing as Expression
            // sequence and create a synthetically named parameter sequence with the expressions in the sequence as
            // values.

            final ExpressionSequence parametersExpSeq =
                    ExpressionSequenceUtils.internalParseExpressionSequence(parametersStr);

            if (parametersExpSeq != null) {
                final AssignationSequence parametersAsSeqFromExp =
                        createSyntheticallyNamedParameterSequence(parametersExpSeq);
                return new FragmentExpression(templateNameExpression, fragmentSpecExpression, parametersAsSeqFromExp, true);
            }

            // The parameters str is not parsable neither as an assignation sequence nor as an expression sequence,
            // so we can come to the conclusion it is wrong.

            return null;

        }

        return new FragmentExpression(templateNameExpression, fragmentSpecExpression, null, false);

    }



    private static Expression parseDefaultAsLiteral(final String input) {

        if (StringUtils.isEmptyOrWhitespace(input)) {
            return null;
        }

        final Expression expr = Expression.parse(input);
        if (expr == null) {
            return Expression.parse(TextLiteralExpression.wrapStringIntoLiteral(input));
        }
        return expr;

    }





    private static int indexOfLastParenthesesGroup(final String input) {

        final int inputLen = input.length();
        final char finalC = input.charAt(inputLen - 1);
        if (finalC != ')') {
            // If there are parentheses, the last char must be an ending one.
            return -1;
        }
        int parenLevel = 1;
        for (int i = inputLen - 2; i >= 0; i--) {
            final char c = input.charAt(i);
            if (c == '(') {
                parenLevel--;
                if (parenLevel == 0) {
                    // We have closed a parenthesis at level 0, this might be what we were looking for.
                    if (i == (inputLen - 2)) {
                        // These are not real parameters, but "()", which might be a "text()" node selector.
                        return -1;
                    }
                    return i;
                }
            } else if (c == ')') {
                parenLevel++;
            }
        }
        // Cannot parse: will never be able to determine whether there are parameters or not, because they aren't
        // correctly closed. Just return -1 as if we didn't find parentheses at all.
        return -1;

    }


    private static AssignationSequence createSyntheticallyNamedParameterSequence(final ExpressionSequence expSeq) {

        final List assignations = new ArrayList(expSeq.size() + 2);

        int argIndex = 0;
        for (final IStandardExpression expression : expSeq.getExpressions()) {
            final IStandardExpression parameterName =
                    Expression.parse(TextLiteralExpression.wrapStringIntoLiteral(UNNAMED_PARAMETERS_PREFIX + argIndex++));
            assignations.add(new Assignation(parameterName, expression));
        }

        return new AssignationSequence(assignations);

    }










    static Fragment executeFragmentExpression(
            final IExpressionContext context,
            final FragmentExpression expression, final StandardExpressionExecutionContext expContext) {

        if (!(context instanceof ITemplateContext)) {
            throw new TemplateProcessingException(
                    "Cannot evaluate expression \"" + expression + "\". Fragment expressions " +
                    "can only be evaluated in a template-processing environment (as a part of an in-template expression) " +
                    "where processing context is an implementation of " + ITemplateContext.class.getClass() + ", which it isn't (" +
                    context.getClass().getName() + ")");
        }

        if (expression == EMPTY_FRAGMENT_EXPRESSION) {
            return Fragment.EMPTY_FRAGMENT;
        }

        return resolveExecutedFragmentExpression(
                (ITemplateContext) context,
                createExecutedFragmentExpression(context, expression, expContext),
                // By default we will NOT consider a non existing template a failure, so that we give the system the chance
                // to return null here (in exchange for a call to resource.exists()). So this false will always be
                // applied in scenarios such as when the fragment expression is used as a parameter in a larger
                // fragment expression: "template : fragment (part=~{expr})"
                false);
    }



    public static ExecutedFragmentExpression createExecutedFragmentExpression(
            final IExpressionContext context,
            final FragmentExpression expression, final StandardExpressionExecutionContext expContext) {

        Validate.notNull(context, "Context cannot be null");
        Validate.notNull(expression, "Fragment Expression cannot be null");

        if (logger.isTraceEnabled()) {
            logger.trace("[THYMELEAF][{}] Evaluating fragment: \"{}\"", TemplateEngine.threadIndex(), expression.getStringRepresentation());
        }


        /*
         * CHECK THE EMPTY EXPRESSION FRAGMENT
         */
        if (expression == EMPTY_FRAGMENT_EXPRESSION) {
            return ExecutedFragmentExpression.EMPTY_EXECUTED_FRAGMENT_EXPRESSION;
        }


        /*
         * COMPUTE THE TEMPLATE NAME
         */
        final IStandardExpression templateNameExpression = expression.getTemplateName();
        final Object templateNameExpressionResult;
        if (templateNameExpression != null) {
            // Note we will apply restricted variable access for resolving template names in fragment specs. This
            // protects against the possibility of code injection attacks from request parameters.
            templateNameExpressionResult = templateNameExpression.execute(context, StandardExpressionExecutionContext.RESTRICTED);
            // We actually DO allow this expression to return null. It can be a way to signal we want to execute the
            // fragment on the current template.
        } else {
            // If template name expression is null, we will execute the fragment on the "current" template
            templateNameExpressionResult = null;
        }


        /*
         * RESOLVE FRAGMENT PARAMETERS if specified (null if not)
         */
        final Map fragmentParameters =
                createExecutedFragmentExpressionParameters(context, expression.getParameters(), expression.hasSyntheticParameters(), expContext);


        /*
         * COMPUTE THE FRAGMENT SELECTOR
         */
        final Object fragmentSelectorExpressionResult;
        if (expression.hasFragmentSelector()) {
            // Note we will NOT apply restricted variable access for resolving fragment selectors
            fragmentSelectorExpressionResult = expression.getFragmentSelector().execute(context, expContext);
            // We actually DO allow this expression to return null. It can be a way to signal we want to use the entire
            // template as a fragment
        } else {
            fragmentSelectorExpressionResult = null;
        }


        /*
         * CREATE the final resulting object
         */
        return new ExecutedFragmentExpression(
                expression,
                templateNameExpressionResult, fragmentSelectorExpressionResult,
                fragmentParameters, expression.hasSyntheticParameters());

    }



    private static Map createExecutedFragmentExpressionParameters(
            final IExpressionContext context,
            final AssignationSequence parameters, final boolean syntheticParameters,
            final StandardExpressionExecutionContext expContext) {

        if (parameters == null || parameters.size() == 0) {
            return null;
        }

        final Map parameterValues = new HashMap(parameters.size() + 2);
        final List assignationValues = parameters.getAssignations();
        final int assignationValuesLen = assignationValues.size();

        for (int i = 0; i < assignationValuesLen; i++) {

            final Assignation assignation = assignationValues.get(i);

            final IStandardExpression parameterNameExpr = assignation.getLeft();

            final String parameterName;
            if (!syntheticParameters) {
                final Object parameterNameValue = parameterNameExpr.execute(context, expContext);
                parameterName = (parameterNameValue == null ? null : parameterNameValue.toString());
            } else {
                // Parameters are synthetic so we know this is a mere literal like "_argX", no need to perform an exec
                parameterName = ((TextLiteralExpression)parameterNameExpr).getValue().getValue();
            }

            final IStandardExpression parameterValueExpr = assignation.getRight();
            final Object parameterValueValue = parameterValueExpr.execute(context, expContext);

            parameterValues.put(parameterName, parameterValueValue);

        }

        return parameterValues;

    }





    public static Fragment resolveExecutedFragmentExpression(
            final ITemplateContext context, final ExecutedFragmentExpression executedFragmentExpression,
            final boolean failIfNotExists) {

        if (executedFragmentExpression == ExecutedFragmentExpression.EMPTY_EXECUTED_FRAGMENT_EXPRESSION) {
            return Fragment.EMPTY_FRAGMENT;
        }

        final IEngineConfiguration configuration = context.getConfiguration();

        /*
         * COMPUTE template name as String
         */
        String templateName = resolveTemplateName(executedFragmentExpression);


        /*
         * COMPUTE fragment selector as String
         */
        final Set fragments = resolveFragments(executedFragmentExpression);


        /*
         * RESOLVE THE FRAGMENT MODEL by using the TemplateManager. This means the fragment will be parsed and maybe
         * cached, and we will be returned an immutable model object
         */
        List templateNameStack = null;
        // scan the template stack if template name is 'this' or an empty name is being used
        if (StringUtils.isEmptyOrWhitespace(templateName)) {

            if (fragments == null || fragments.isEmpty()) {
                return null;
            }

            templateNameStack = new ArrayList(3);
            for (int i = context.getTemplateStack().size() - 1; i >= 0; i--) {
                templateNameStack.add(context.getTemplateStack().get(i).getTemplate());
            }
            templateName = templateNameStack.get(0);
        }

        TemplateModel fragmentModel;
        int i = 0;
        do {
            fragmentModel =
                    configuration.getTemplateManager().parseStandalone(
                            context, templateName, fragments,
                            null,             // we will not force the template mode
                            true,             // use the cache if possible, fragments are from template files
                            failIfNotExists); // depending on the scenario we will consider a non exiting template a fail or not
            i++;
        } while (fragmentModel != null &&               // template not found (only if resolver configuration allows)
                fragmentModel.size() <= 2 &&            // template found, but selector not found
                templateNameStack != null &&            // we have more templates to look into
                i < templateNameStack.size() &&         // we have more templates to look into
                (templateName = templateNameStack.get(i)) != null);  //post test -- need to parse at least 1x

        if (fragmentModel == null) {
            // FragmentExpressions can actually return null, so that this null value can be used in standard expressions
            // such as "~{template} ? ~{default}"
            return null;
        }


        /*
         * We should now check if the resolved fragment actually exists or not (we know the template exists but,
         * did the fragment actually return anything at all?
         */

        final boolean fragmentIsEmpty = (fragmentModel.size() == 2); //only templatestart/end

        if (fragmentIsEmpty) {
            // Fragment is empty, so we should take action: depending on whether we are allowing this or not, we should
            // either fail or simply return null
            if (failIfNotExists) {
                throw new TemplateInputException(
                        "Error resolving fragment: \"" + executedFragmentExpression.fragmentExpression.getStringRepresentation() + "\": " +
                        "template or fragment could not be resolved");
            }
            return null;
        }


        /*
         * RETURN the expected Fragment object
         */
        return new Fragment(fragmentModel, executedFragmentExpression.fragmentParameters, executedFragmentExpression.syntheticParameters);

    }




    public static String resolveTemplateName(final ExecutedFragmentExpression executedFragmentExpression) {

        final Object templateNameObject = executedFragmentExpression.templateNameExpressionResult;
        if (templateNameObject == null) {
            // If template name expression result is null, we will execute the fragment on the "current" template
            return null;
        }
        final String evaluatedTemplateName = templateNameObject.toString();
        if (TEMPLATE_NAME_CURRENT_TEMPLATE.equals(evaluatedTemplateName)) {
            // Template name is "this" and therefore we are including a fragment from the same template.
            return null;
        }
        return templateNameObject.toString();

    }




    public static Set resolveFragments(final ExecutedFragmentExpression executedFragmentExpression) {

        final Object fragmentSelectorObject = executedFragmentExpression.fragmentSelectorExpressionResult;
        if (fragmentSelectorObject != null) {

            String fragmentSelector = fragmentSelectorObject.toString();

            if (fragmentSelector.length() > 3 &&
                    fragmentSelector.charAt(0) == '[' && fragmentSelector.charAt(fragmentSelector.length() - 1) == ']' &&
                    fragmentSelector.charAt(fragmentSelector.length() - 2) != '\'') {
                // For legacy compatibility reasons, we allow fragment DOM Selector expressions to be specified
                // between brackets. Just remove them.
                fragmentSelector = fragmentSelector.substring(1, fragmentSelector.length() - 1).trim();
            }

            if (fragmentSelector.trim().length() > 0) {
                return Collections.singleton(fragmentSelector);
            }

        }

        return null;

    }





    public static final class ExecutedFragmentExpression {

        public static final ExecutedFragmentExpression EMPTY_EXECUTED_FRAGMENT_EXPRESSION =
                new ExecutedFragmentExpression(EMPTY_FRAGMENT_EXPRESSION, null, null, null, false);

        private final FragmentExpression fragmentExpression;
        private final Object templateNameExpressionResult;
        private final Object fragmentSelectorExpressionResult;
        private final Map fragmentParameters;
        private final boolean syntheticParameters;

        ExecutedFragmentExpression(
                final FragmentExpression fragmentExpression,
                final Object templateNameExpressionResult, final Object fragmentSelectorExpressionResult,
                final Map fragmentParameters, final boolean syntheticParameters) {
            super();
            this.fragmentExpression = fragmentExpression;
            this.templateNameExpressionResult = templateNameExpressionResult;
            this.fragmentSelectorExpressionResult = fragmentSelectorExpressionResult;
            this.fragmentParameters = fragmentParameters;
            this.syntheticParameters = syntheticParameters;
        }

        FragmentExpression getFragmentExpression() {
            return this.fragmentExpression;
        }

        public Object getTemplateNameExpressionResult() {
            return this.templateNameExpressionResult;
        }

        public Object getFragmentSelectorExpressionResult() {
            return this.fragmentSelectorExpressionResult;
        }

        public Map getFragmentParameters() {
            return this.fragmentParameters;
        }

        public boolean hasSyntheticParameters() {
            return this.syntheticParameters;
        }

    }

    
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy