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

org.thymeleaf.DOMDocumentProcessor Maven / Gradle / Ivy

/*
 * =============================================================================
 * 
 *   Copyright (c) 2011, 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;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.exceptions.NoAvailableProcessorException;
import org.thymeleaf.exceptions.ParsingException;
import org.thymeleaf.processor.SubstitutionTag;
import org.thymeleaf.processor.attr.AttrProcessResult;
import org.thymeleaf.processor.attr.IAttrProcessor;
import org.thymeleaf.processor.tag.ITagProcessor;
import org.thymeleaf.processor.tag.TagProcessResult;
import org.thymeleaf.templateresolver.TemplateResolution;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

/**
 * 
 * @author Daniel Fernández
 * 
 * @since 1.0
 *
 */
final class DOMDocumentProcessor {
    
    private static final Logger logger = LoggerFactory.getLogger(DOMDocumentProcessor.class);
    

    static DocumentType transform(final Arguments arguments, final TemplateResolution templateResolution, final Document document) {

        final TemplateMode templateMode = templateResolution.getTemplateMode();
        
        final NodeList docChildren = document.getChildNodes();
        
        DocumentType docType = null;
        Element rootElement = null;
        
        if (docChildren.getLength() < 1) {
            throw new ParsingException(
                    "Invalid document structure: no root element found.");
        }
        
        if (docChildren.getLength() == 1) {
            
            if (docChildren.item(0).getNodeType() != Node.ELEMENT_NODE) {
                throw new ParsingException(
                        "Invalid document structure: If only one top-level node exists, this " +
                        "has to be the root element.");
            }
            
            if (templateMode.isXHTML() || templateMode.isHTML5()) {
                throw new ParsingException(
                        "Invalid document structure: Web templates (XHTML/HTML5) must include a DOCTYPE declaration.");
            }
            
            rootElement = (Element) docChildren.item(0);
            
        } else if (docChildren.getLength() == 2) {
            
            if (docChildren.item(0).getNodeType() != Node.DOCUMENT_TYPE_NODE) {
                throw new ParsingException(
                        "Invalid document structure: If two top-level nodes exist, the first one " +
                        "has to be the DOCTYPE.");
            }
            
            if (docChildren.item(1).getNodeType() != Node.ELEMENT_NODE) {
                throw new ParsingException(
                        "Invalid document structure: If two top-level nodes exist, the second one " +
                        "has to be the root element.");
            }
            
            docType = (DocumentType) docChildren.item(0);
            
            final String publicId = docType.getPublicId();
            final String systemId = docType.getSystemId();
            
            if (templateMode.isHTML5()) {
                if (!((publicId == null || publicId.trim().equals("")) &&
                      (systemId == null || systemId.trim().equals("") || systemId.trim().equalsIgnoreCase(Standards.HTML_5_LEGACY_WILDCARD_SYSTEMID.getValue())))) {
                    throw new ParsingException(
                            "Template is being processed in " + templateMode + " mode. Only " +
                            "\"\" and \"\" " +
                            "are allowed."); 
                }
            }
            if (templateMode.isXHTML()) {
                if (systemId == null || systemId.trim().equals("")) {
                      throw new ParsingException(
                              "Template is being processed in " + templateMode + " mode. A " +
                              "correct 'PUBLIC' or 'SYSTEM' DOCTYPE declaration is required."); 
                  }
            }
            
            
            rootElement = (Element) docChildren.item(1);
            
        } else {
            throw new ParsingException(
                "Invalid document structure: No more than two top-level elements are allowed " +
                "(either root element, or doctype + root element).");
        }
        

        /*
         * Start traversing the node tree and applying transformations
         */
        transformNode(arguments, templateResolution, document, rootElement);
        
        return docType;
        
    }

    
    
    
    private static void transformNode(final Arguments arguments,
            final TemplateResolution templateResolution, 
            final Document document, final Node node) {
        
        final Configuration configuration = arguments.getConfiguration();
        final Set processedAttributeNames = configuration.getProcessedAttrNames();
        
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            
            final Element element = (Element) node;
            final String elementName = element.getNodeName().toLowerCase();
            
            boolean tagIsRemoved = false;
            boolean tagChildrenAreRemoved = false;
            boolean tagIsSubstituted = false;
            
            final Map localVariables = new LinkedHashMap();
            final List substitutionTags = new ArrayList();
            Object selectionTarget = null;
            boolean selectionTargetSet = false;
            
            if (configuration.getProcessedTagNames().contains(elementName)) {
                
                final ITagProcessor elementProcessor = configuration.getTagProcessor(element);
                if (elementProcessor == null) {
                    
                    if (configuration.tagNameHasPrefix(elementName) && !configuration.isLenient()) {
                        throw new NoAvailableProcessorException(
                                "No processor in dialect found for tag \"" + elementName + "\"");
                    }
                    
                } else {
                    
                    if (logger.isTraceEnabled()) {
                        logger.trace("[THYMELEAF][{}] TAG: Processing tag: \"{}\"", TemplateEngine.threadIndex(), elementName);
                    }
                    
                    final TagProcessResult tagProcessResult =
                        elementProcessor.process(arguments, templateResolution, document, element);
                    tagIsRemoved = tagProcessResult.getAction().isTagRemoved();
                    tagChildrenAreRemoved = tagProcessResult.getAction().isChildrenRemoved();
                    tagIsSubstituted = tagProcessResult.getAction().isTagSubstituted();
                    localVariables.putAll(tagProcessResult.getLocalVariables());
                    substitutionTags.addAll(tagProcessResult.getSubstitutionTags());
                    if (tagProcessResult.isSelectionTargetSet()) {
                        selectionTargetSet = true;
                        selectionTarget = tagProcessResult.getSelectionTarget();
                    }
                    
                }
                
            } else if (configuration.tagNameHasPrefix(elementName) && !configuration.isLenient()) {
                throw new NoAvailableProcessorException(
                        "No processor in dialect found for tag \"" + elementName + "\"");
            }

            
            /*
             * If tag is not removed after tag execution, we should process attributes
             */
            if (!tagIsRemoved) {
                
                final Map attrProcessorsToExecute = new LinkedHashMap(); 

                /*
                 * First, we create a list with the attr processors that should be executed.
                 */
                final NamedNodeMap attributes = element.getAttributes();
                for (int i = 0; i < attributes.getLength(); i++) {

                    final Attr attribute = (Attr) attributes.item(i);
                    final String attributeName = attribute.getName().toLowerCase();
                    
                    if (processedAttributeNames.contains(attributeName)) {
                        final IAttrProcessor attrProcessor = configuration.getAttrProcessor(element, attribute);
                        if (attrProcessor != null) {
                            attrProcessorsToExecute.put(attributeName, attrProcessor);
                        } else if (configuration.attrNameHasPrefix(attributeName) && !configuration.isLenient()) {
                            throw new NoAvailableProcessorException(
                                    "No processor in dialect found for attribute \"" + attributeName + "\" with value = \"" + attribute.getValue() + "\"");
                        }
                    } else if (configuration.attrNameHasPrefix(attributeName) && !configuration.isLenient()) {
                        throw new NoAvailableProcessorException(
                                "No processor in dialect found for attribute \"" + attributeName + "\" with value = \"" + attribute.getValue() + "\"");
                    }
                    
                }
                
                /*
                 * Attr processors are sorted according to their precedence
                 */
                final Map orderedAttrProcessorsToExecute = 
                    new TreeMap(
                            new AttrProcessorMapComparator(attrProcessorsToExecute));
                orderedAttrProcessorsToExecute.putAll(attrProcessorsToExecute);


                /*
                 * Once ordered, attr processors are executed on their corresponding attribute nodes
                 */
                for (final Map.Entry attrProcessorEntry :  orderedAttrProcessorsToExecute.entrySet()) {

                    if (tagIsRemoved) {
                        break;
                    }
                    
                    final String attributeName = attrProcessorEntry.getKey();
                    final IAttrProcessor attrProcessor = attrProcessorEntry.getValue();
                    
                    final Attr attribute = element.getAttributeNode(attributeName);
                    
                    if (logger.isTraceEnabled()) {
                        logger.trace("[THYMELEAF][{}] ATTRIBUTE: Processing attribute: \"{}\" with value \"{}\"", new Object[] {TemplateEngine.threadIndex(), attributeName, attribute.getValue()});
                    }
                    
                    /*
                     * Compute the Arguments object to be used for attribute evaluation
                     */
                    Arguments attrArguments = null;
                    if (localVariables.isEmpty()) {
                        if (!selectionTargetSet) {
                            attrArguments = arguments;
                        } else {
                            attrArguments = arguments.setSelectionTarget(selectionTarget);
                        }
                    } else {
                        if (!selectionTargetSet) {
                            attrArguments = arguments.addLocalVariables(localVariables);
                        } else {
                            attrArguments = arguments.addLocalVariablesAndSetSelectionTarget(localVariables, selectionTarget);
                        }
                    }
                    

                    final AttrProcessResult attrProcessResult =
                        attrProcessor.process(attrArguments, templateResolution, document, element, attribute);
                    tagIsRemoved = attrProcessResult.getAction().isTagRemoved();
                    tagChildrenAreRemoved = attrProcessResult.getAction().isChildrenRemoved();
                    tagIsSubstituted = attrProcessResult.getAction().isTagSubstituted();
                    localVariables.putAll(attrProcessResult.getLocalVariables());
                    substitutionTags.addAll(attrProcessResult.getSubstitutionTags());
                    if (attrProcessResult.isSelectionTargetSet()) {
                        selectionTargetSet = true;
                        selectionTarget = attrProcessResult.getSelectionTarget();
                    }
                    
                    if (attrProcessResult.getAction().isAttrRemoved()) {
                        element.removeAttribute(attributeName);
                    }
                    
                }
                
            }
            
            /*
             * Compute the Arguments object to be used for children evaluation
             */
            Arguments childrenArguments = null;
            if (localVariables.isEmpty()) {
                if (!selectionTargetSet) {
                    childrenArguments = arguments;
                } else {
                    childrenArguments = arguments.setSelectionTarget(selectionTarget);
                }
            } else {
                if (!selectionTargetSet) {
                    childrenArguments = arguments.addLocalVariables(localVariables);
                } else {
                    childrenArguments = arguments.addLocalVariablesAndSetSelectionTarget(localVariables, selectionTarget);
                }
            }
            
            if (!tagIsRemoved) {

                if (tagChildrenAreRemoved) {

                    final NodeList children = node.getChildNodes();
                    final List childNodes = new ArrayList();
                    for (int i = 0; i < children.getLength(); i++) {
                        // In case nodes are deleted along the way, we create a list in order not
                        // to have to rely on the NodeList object.
                        childNodes.add(children.item(i));
                    }
                    for (final Node child : childNodes) {
                        element.removeChild(child);
                    }
                    
                } else {
    
                    final NodeList children = node.getChildNodes();
                    final List childNodes = new ArrayList();
                    for (int i = 0; i < children.getLength(); i++) {
                        // In case nodes are deleted along the way, we create a list in order not
                        // to have to rely on the NodeList object.
                        childNodes.add(children.item(i));
                    }
                    for (final Node child : childNodes) {
                        transformNode(childrenArguments, templateResolution, document, child);
                    }
                    
                }
                
            } else {

                if (tagChildrenAreRemoved) {

                    final NodeList children = node.getChildNodes();
                    final List childNodes = new ArrayList();
                    for (int i = 0; i < children.getLength(); i++) {
                        // In case nodes are deleted along the way, we create a list in order not
                        // to have to rely on the NodeList object.
                        childNodes.add(children.item(i));
                    }
                    for (final Node child : childNodes) {
                        element.removeChild(child);
                    }
                    
                    if (tagIsSubstituted) {

                        for (final SubstitutionTag substitutionTag : substitutionTags) {

                            element.getParentNode().insertBefore(substitutionTag.getTag(), element);
                        }
                        
                        for (final SubstitutionTag substitutionTag : substitutionTags) {
                            
                            final Map substitutionLocalVariables = new LinkedHashMap();
                            substitutionLocalVariables.putAll(localVariables);
                            substitutionLocalVariables.putAll(substitutionTag.getLocalVariables());
                            
                            /*
                             * Compute the Arguments object to be used for substitution evaluation
                             */
                            Arguments substitutionArguments = null;
                            if (substitutionLocalVariables.isEmpty()) {
                                if (!selectionTargetSet) {
                                    substitutionArguments = arguments;
                                } else {
                                    substitutionArguments = arguments.setSelectionTarget(selectionTarget);
                                }
                            } else {
                                if (!selectionTargetSet) {
                                    substitutionArguments = arguments.addLocalVariables(substitutionLocalVariables);
                                } else {
                                    substitutionArguments = arguments.addLocalVariablesAndSetSelectionTarget(substitutionLocalVariables, selectionTarget);
                                }
                            }
                            
                            transformNode(substitutionArguments, templateResolution, document, substitutionTag.getTag());
                            
                        }
                            
                    }
                    
                    element.getParentNode().removeChild(element);
                    
                    
                } else {
                    
                    final NodeList children = node.getChildNodes();
                    final List childNodes = new ArrayList();
                    for (int i = 0; i < children.getLength(); i++) {
                        // In case nodes are deleted along the way, we create a list in order not
                        // to have to rely on the NodeList object.
                        childNodes.add(children.item(i));
                    }
                    for (final Node child : childNodes) {
                        element.removeChild(child);
                        element.getParentNode().insertBefore(child, element);
                    }
                    element.getParentNode().removeChild(element);
                    for (final Node child : childNodes) {
                        transformNode(childrenArguments, templateResolution, document, child);
                    }
                    
                }
                
            }
            
            
            if (!tagIsRemoved && !configuration.isLenient() && configuration.xmlNsAttrName() != null) {
                element.removeAttribute(configuration.xmlNsAttrName());
            }
            
            if (!tagIsRemoved && 
                nodeIsEmpty(element) && 
                (templateResolution.getTemplateMode().isXHTML() || templateResolution.getTemplateMode().isHTML5())) {
                
                /*
                 * If we are processing an XHTML template, we have to make sure that
                 * only certain tags get minimized (because the XHTML specification forbids
                 * many tags like