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

org.thymeleaf.engine.TemplateModelController Maven / Gradle / Ivy

Go to download

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

There is a newer version: 3.0.12.2
Show newest version
/*
 * =============================================================================
 *
 *   Copyright (c) 2011-2018, 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.engine;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.context.IEngineContext;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.model.ICDATASection;
import org.thymeleaf.model.ICloseElementTag;
import org.thymeleaf.model.IComment;
import org.thymeleaf.model.IDocType;
import org.thymeleaf.model.IOpenElementTag;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.model.IProcessingInstruction;
import org.thymeleaf.model.IStandaloneElementTag;
import org.thymeleaf.model.ITemplateEvent;
import org.thymeleaf.model.IText;
import org.thymeleaf.model.IXMLDeclaration;
import org.thymeleaf.templatemode.TemplateMode;


/*
 * This is an internal class for gathering a sequence of template events into a Model object. This will
 * be used from ProcessorTemplateHandler for gathering iterated sequences, as well as models that are to be
 * passed to ElementModelProcessors.
 * 
 * NOTE there is no need to implement ITemplateHandler or extend AbstractTemplateHandler.
 *
 * @author Daniel Fernandez
 * @since 3.0.0
 *
 */
final class TemplateModelController {

    static final int DEFAULT_MODEL_LEVELS = 25;

    enum SkipBody {

        PROCESS(true, true, true),
        SKIP_ALL(false, false, false),
        SKIP_ELEMENTS(false, true, false),
        PROCESS_ONE_ELEMENT(true, true, true);

        final boolean processElements;
        final boolean processNonElements;
        final boolean processChildren;

        SkipBody(final boolean processElements, final boolean processNonElements, final boolean processChildren) {
            this.processElements = processElements;
            this.processNonElements = processNonElements;
            this.processChildren = processChildren;
        }

    }

    // This is a set containing all the names of the elements for which, when iterated, we should preserve
    // the preceding whitespace if it exists so that resulting markup is more readable. Note they are all block
    // elements or, at least, elements for which preceding whitespace should not matter
    private static final Set ITERATION_WHITESPACE_APPLICABLE_ELEMENT_NAMES =
            new HashSet(Arrays.asList(new HTMLElementName[] {
                    ElementNames.forHTMLName("address"), ElementNames.forHTMLName("article"), ElementNames.forHTMLName("aside"),
                    ElementNames.forHTMLName("audio"), ElementNames.forHTMLName("blockquote"), ElementNames.forHTMLName("canvas"),
                    ElementNames.forHTMLName("dd"), ElementNames.forHTMLName("div"), ElementNames.forHTMLName("dl"),
                    ElementNames.forHTMLName("dt"), ElementNames.forHTMLName("fieldset"), ElementNames.forHTMLName("figcaption"),
                    ElementNames.forHTMLName("figure"), ElementNames.forHTMLName("footer"),ElementNames.forHTMLName("form"),
                    ElementNames.forHTMLName("h1"), ElementNames.forHTMLName("h2"), ElementNames.forHTMLName("h3"),
                    ElementNames.forHTMLName("h4"), ElementNames.forHTMLName("h5"), ElementNames.forHTMLName("h6"),
                    ElementNames.forHTMLName("header"), ElementNames.forHTMLName("hgroup"), ElementNames.forHTMLName("hr"),
                    ElementNames.forHTMLName("li"), ElementNames.forHTMLName("main"), ElementNames.forHTMLName("nav"),
                    ElementNames.forHTMLName("noscript"), ElementNames.forHTMLName("ol"), ElementNames.forHTMLName("option"),
                    ElementNames.forHTMLName("output"), ElementNames.forHTMLName("p"), ElementNames.forHTMLName("pre"),
                    ElementNames.forHTMLName("section"), ElementNames.forHTMLName("table"), ElementNames.forHTMLName("tbody"),
                    ElementNames.forHTMLName("td"), ElementNames.forHTMLName("tfoot"), ElementNames.forHTMLName("th"),
                    ElementNames.forHTMLName("tr"), ElementNames.forHTMLName("ul"), ElementNames.forHTMLName("video")
            }));




    private final IEngineConfiguration configuration;
    private final TemplateMode templateMode;
    private final ProcessorTemplateHandler processorTemplateHandler;
    private final IEngineContext context;

    private TemplateFlowController templateFlowController;

    private AbstractGatheringModelProcessable gatheredModel;

    private SkipBody skipBody;
    private SkipBody[] skipBodyByLevel;
    private boolean[] skipCloseTagByLevel;
    private IProcessableElementTag[] unskippedFirstElementByLevel;

    // These two variables will help us keep account of what events have been triggered before an iteration, in
    // case we want to apply prettifying to the surrounding white spaces of an iterated piece of markup
    private ITemplateEvent lastEvent = null;
    private ITemplateEvent secondToLastEvent = null;

    private int modelLevel;


    TemplateModelController(
            final IEngineConfiguration configuration, final TemplateMode templateMode,
            final ProcessorTemplateHandler processorTemplateHandler, final IEngineContext context) {

        super();

        this.configuration = configuration;
        this.templateMode = templateMode;
        this.processorTemplateHandler = processorTemplateHandler;
        this.context = context;

        this.gatheredModel = null;

        this.skipBodyByLevel = new SkipBody[DEFAULT_MODEL_LEVELS];
        this.skipBodyByLevel[this.modelLevel] = SkipBody.PROCESS;
        this.skipBody = this.skipBodyByLevel[this.modelLevel];

        this.skipCloseTagByLevel = new boolean[DEFAULT_MODEL_LEVELS];
        this.skipCloseTagByLevel[this.modelLevel] = false;

        this.unskippedFirstElementByLevel = new IProcessableElementTag[DEFAULT_MODEL_LEVELS];
        this.unskippedFirstElementByLevel[this.modelLevel] = null;

        this.modelLevel = 0;

    }


    void setTemplateFlowController(final TemplateFlowController templateFlowController) {
        this.templateFlowController = templateFlowController;
    }


    int getModelLevel() {
        return this.modelLevel;
    }


    void startGatheringDelayedModel(
            final IOpenElementTag firstTag, final ProcessorExecutionVars processorExecutionVars) {

        this.modelLevel--;

        final SkipBody gatheredSkipBody = this.skipBodyByLevel[this.modelLevel];
        final boolean gatheredSkipCloseTagByLevel = this.skipCloseTagByLevel[this.modelLevel];

        this.gatheredModel =
                new GatheringModelProcessable(
                        this.configuration, this.processorTemplateHandler, this.context,
                        this, this.templateFlowController,
                        gatheredSkipBody, gatheredSkipCloseTagByLevel, processorExecutionVars);

        this.gatheredModel.gatherOpenElement(firstTag);

    }


    void startGatheringDelayedModel(
            final IStandaloneElementTag firstTag, final ProcessorExecutionVars processorExecutionVars) {

        SkipBody gatheredSkipBody = this.skipBodyByLevel[this.modelLevel];
        gatheredSkipBody = (gatheredSkipBody == SkipBody.SKIP_ELEMENTS ? SkipBody.PROCESS_ONE_ELEMENT : gatheredSkipBody);
        final boolean gatheredSkipCloseTagByLevel = this.skipCloseTagByLevel[this.modelLevel];

        this.gatheredModel =
                new GatheringModelProcessable(
                        this.configuration, this.processorTemplateHandler, this.context,
                        this, this.templateFlowController,
                        gatheredSkipBody, gatheredSkipCloseTagByLevel, processorExecutionVars);

        this.gatheredModel.gatherStandaloneElement(firstTag);

    }


    void startGatheringIteratedModel(
            final IOpenElementTag firstTag, final ProcessorExecutionVars processorExecutionVars,
            final String iterVariableName, final String iterStatusVariableName, final Object iteratedObject) {

        this.modelLevel--;

        final SkipBody gatheredSkipBody = this.skipBodyByLevel[this.modelLevel];
        final boolean gatheredSkipCloseTagByLevel = this.skipCloseTagByLevel[this.modelLevel];

        final Text precedingWhitespace = computeWhiteSpacePrecedingIteration(firstTag.getElementDefinition().elementName);

        this.gatheredModel =
                new IteratedGatheringModelProcessable(
                        this.configuration, this.processorTemplateHandler, this.context,
                        this, this.templateFlowController,
                        gatheredSkipBody, gatheredSkipCloseTagByLevel, processorExecutionVars,
                        iterVariableName, iterStatusVariableName, iteratedObject, precedingWhitespace);

        this.gatheredModel.gatherOpenElement(firstTag);

    }


    void startGatheringIteratedModel(
            final IStandaloneElementTag firstTag, final ProcessorExecutionVars processorExecutionVars,
            final String iterVariableName, final String iterStatusVariableName, final Object iteratedObject) {

        SkipBody gatheredSkipBody = this.skipBodyByLevel[this.modelLevel];
        gatheredSkipBody = (gatheredSkipBody == SkipBody.SKIP_ELEMENTS ? SkipBody.PROCESS_ONE_ELEMENT : gatheredSkipBody);
        final boolean gatheredSkipCloseTagByLevel = this.skipCloseTagByLevel[this.modelLevel];

        final Text precedingWhitespace = computeWhiteSpacePrecedingIteration(firstTag.getElementDefinition().elementName);

        this.gatheredModel =
                new IteratedGatheringModelProcessable(
                        this.configuration, this.processorTemplateHandler, this.context,
                        this, this.templateFlowController,
                        gatheredSkipBody, gatheredSkipCloseTagByLevel, processorExecutionVars,
                        iterVariableName, iterStatusVariableName, iteratedObject, precedingWhitespace);

        this.gatheredModel.gatherStandaloneElement(firstTag);

    }


    GatheringModelProcessable createStandaloneEquivalentModel(
            final StandaloneElementTag standaloneElementTag, final ProcessorExecutionVars processorExecutionVars) {

        SkipBody gatheredSkipBody = this.skipBodyByLevel[this.modelLevel];
        gatheredSkipBody = (gatheredSkipBody == SkipBody.SKIP_ELEMENTS ? SkipBody.PROCESS_ONE_ELEMENT : gatheredSkipBody);
        final boolean gatheredSkipCloseTagByLevel = this.skipCloseTagByLevel[this.modelLevel];

        final OpenElementTag openTag =
                new OpenElementTag(
                        standaloneElementTag.templateMode, standaloneElementTag.elementDefinition,
                        standaloneElementTag.elementCompleteName, standaloneElementTag.attributes, standaloneElementTag.synthetic,
                        standaloneElementTag.templateName, standaloneElementTag.line, standaloneElementTag.col);
        final CloseElementTag closeTag =
                new CloseElementTag(
                        standaloneElementTag.templateMode, standaloneElementTag.elementDefinition,
                        standaloneElementTag.elementCompleteName, null, standaloneElementTag.synthetic, false,
                        standaloneElementTag.templateName, standaloneElementTag.line, standaloneElementTag.col);

        final GatheringModelProcessable equivalentModel =
                new GatheringModelProcessable(
                        this.configuration, this.processorTemplateHandler, this.context,
                        this, this.templateFlowController,
                        gatheredSkipBody, gatheredSkipCloseTagByLevel, processorExecutionVars);

        equivalentModel.gatherOpenElement(openTag);
        equivalentModel.gatherCloseElement(closeTag);

        return equivalentModel;

    }


    boolean isGatheringFinished() {
        return this.gatheredModel != null && this.gatheredModel.isGatheringFinished();
    }


    IGatheringModelProcessable getGatheredModel() {
        return this.gatheredModel;
    }


    void resetGathering() {
        this.gatheredModel = null;
    }



    void skip(final SkipBody skipBody, final boolean skipCloseTag) {
        skipBody(skipBody);
        skipCloseTag(skipCloseTag);
    }

    private void skipBody(final SkipBody skipBody) {
        this.skipBodyByLevel[this.modelLevel] = skipBody;
        this.skipBody = skipBody;
    }


    private void skipCloseTag(final boolean skipCloseTag) {
        if (!skipCloseTag) {
            return;
        }
        if (this.modelLevel == 0) {
            throw new TemplateProcessingException("Cannot set containing close tag to skip when model level is zero");
        }
        this.skipCloseTagByLevel[this.modelLevel - 1] = true;
    }



    private void increaseModelLevel(final IOpenElementTag openElementTag) {
        this.modelLevel++;
        if (this.skipBodyByLevel.length == this.modelLevel) {
            this.skipBodyByLevel = Arrays.copyOf(this.skipBodyByLevel, this.skipBodyByLevel.length + DEFAULT_MODEL_LEVELS/2);
            this.skipCloseTagByLevel = Arrays.copyOf(this.skipCloseTagByLevel, this.skipCloseTagByLevel.length + DEFAULT_MODEL_LEVELS/2);
            this.unskippedFirstElementByLevel = Arrays.copyOf(this.unskippedFirstElementByLevel, this.unskippedFirstElementByLevel.length + DEFAULT_MODEL_LEVELS/2);
        }
        skipBody(this.skipBody.processChildren ? SkipBody.PROCESS : SkipBody.SKIP_ALL);
        this.skipCloseTagByLevel[this.modelLevel] = false;
        this.unskippedFirstElementByLevel[this.modelLevel] = null;
        if (this.context != null) {
            this.context.increaseLevel();
            this.context.setElementTag(openElementTag);
        }
    }


    private void decreaseModelLevel() {
        this.modelLevel--;
        this.skipBody = this.skipBodyByLevel[this.modelLevel];
        if (this.context != null) {
            this.context.decreaseLevel();
        }
    }







    boolean shouldProcessText(final IText text) {
        this.lastEvent = text;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherText(text);
            return false;
        }
        return this.skipBody.processNonElements;
    }


    boolean shouldProcessComment(final IComment comment) {
        this.lastEvent = comment;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherComment(comment);
            return false;
        }
        return this.skipBody.processNonElements;
    }


    boolean shouldProcessCDATASection(final ICDATASection cdataSection) {
        this.lastEvent = cdataSection;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherCDATASection(cdataSection);
            return false;
        }
        return this.skipBody.processNonElements;
    }


    boolean shouldProcessStandaloneElement(final IStandaloneElementTag standaloneElementTag) {
        this.secondToLastEvent = this.lastEvent;
        this.lastEvent = standaloneElementTag;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherStandaloneElement(standaloneElementTag);
            return false;
        }
        boolean process = this.skipBody.processElements;
        if (this.skipBody == SkipBody.PROCESS_ONE_ELEMENT) {
            // This was the first element, the others will be skipped. Let's save it in case it is iterated
            this.unskippedFirstElementByLevel[this.modelLevel] = standaloneElementTag;
            skipBody(SkipBody.SKIP_ELEMENTS);
            process = true;
        }
        if (process) {
            /*
             * INCREASE THE CONTEXT LEVEL so that all local variables created during the execution of processors
             * are available for the rest of the processors as well as the body of the tag
             */
            if (this.context != null) {
                this.context.increaseLevel();
                this.context.setElementTag(standaloneElementTag);
            }
        }
        return process;
    }


    boolean shouldProcessOpenElement(final IOpenElementTag openElementTag) {
        this.secondToLastEvent = this.lastEvent;
        this.lastEvent = openElementTag;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherOpenElement(openElementTag);
            return false;
        }
        boolean process = this.skipBody.processElements;
        if (this.skipBody == SkipBody.PROCESS_ONE_ELEMENT) {
            // This is the first (still unclosed) element, let's save it in case it is iterated
            this.unskippedFirstElementByLevel[this.modelLevel] = openElementTag;
        } else if (this.skipBody == SkipBody.SKIP_ELEMENTS && this.unskippedFirstElementByLevel[this.modelLevel] == openElementTag) {
            // The unskipped first element is being iterated! we should allow its processing
            skipBody(SkipBody.PROCESS_ONE_ELEMENT);
            process = true;
        }
        increaseModelLevel(openElementTag);
        return process;
    }


    boolean shouldProcessCloseElement(final ICloseElementTag closeElementTag) {
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherCloseElement(closeElementTag);
            return false;
        }
        this.lastEvent = closeElementTag;
        decreaseModelLevel();
        if (this.skipBody == SkipBody.PROCESS_ONE_ELEMENT) {
            // This was the first element, the others will be skipped
            skipBody(SkipBody.SKIP_ELEMENTS);
            if (this.skipCloseTagByLevel[this.modelLevel]) {
                this.skipCloseTagByLevel[this.modelLevel] = false;
                return false;
            } else {
                return true;
            }
        }
        if (this.skipCloseTagByLevel[this.modelLevel]) {
            this.skipCloseTagByLevel[this.modelLevel] = false;
            return false;
        }
        return this.skipBody.processElements;
    }


    boolean shouldProcessUnmatchedCloseElement(final ICloseElementTag closeElementTag) {
        this.lastEvent = closeElementTag;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherUnmatchedCloseElement(closeElementTag);
            return false;
        }
        return this.skipBody.processNonElements; // We will treat this as a non-element
    }


    boolean shouldProcessDocType(final IDocType docType) {
        this.lastEvent = docType;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherDocType(docType);
            return false;
        }
        return this.skipBody.processNonElements;
    }


    boolean shouldProcessXMLDeclaration(final IXMLDeclaration xmlDeclaration) {
        this.lastEvent = xmlDeclaration;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherXMLDeclaration(xmlDeclaration);
            return false;
        }
        return this.skipBody.processNonElements;
    }


    boolean shouldProcessProcessingInstruction(final IProcessingInstruction processingInstruction) {
        this.lastEvent = processingInstruction;
        if (this.gatheredModel != null) {
            this.gatheredModel.gatherProcessingInstruction(processingInstruction);
            return false;
        }
        return this.skipBody.processNonElements;
    }




    private Text computeWhiteSpacePrecedingIteration(final ElementName iteratedElementName) {
        if (this.secondToLastEvent == null || !(this.secondToLastEvent instanceof IText)) {
            return null;
        }
        if (this.templateMode == TemplateMode.XML ||
                (this.templateMode == TemplateMode.HTML && ITERATION_WHITESPACE_APPLICABLE_ELEMENT_NAMES.contains(iteratedElementName))) {
            final Text lastEngineText = Text.asEngineText((IText) this.secondToLastEvent);
            if (lastEngineText.isWhitespace()) {
                return lastEngineText;
            }
        }
        return null;
    }

    
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy