Package org.thymeleaf.dom

Source Code of org.thymeleaf.dom.DOMSelector$INodeReferenceChecker

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

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.util.StringUtils;
import org.thymeleaf.util.Validate;


/**
* <p>
*   DOM Selectors model selections of subtrees from Thymeleaf DOM trees.
* </p>
* <p>
*   A common use of these selectors is for including fragments of other templates
*   without the need of these other templates having any Thymeleaf code.
* </p>
* <p>
*   Note this class exists since 2.0.0, but its syntax has been greatly enhanced in 2.1.0.
* </p>
* <h3>Selection features</h3>
* <p>
*   DOM Selector selection operations are based on:
* </p>
* <ul>
*   <li>The <i><b>type of node</b></i>: both tags (<i>elements</i>) and text nodes can be selected.</li>
*   <li>The <i><b>name of the element</b></i> (<i>element</i> = <i>tag</i>).</li>
*   <li>The <i><b>path and depth</b></i> of the node in the DOM tree.</li>
*   <li>The <i><b>attributes</b></i> of the element (if it is an element).</li>
*   <li>The <i><b>index</b></i> of the node: its position among its siblings (<i>brother nodes of the same kind</i>).</li>
*   <li>The <i><b>references</b></i> applied to the node. These references are resolved by means of the
*       specification of an object implementing {@link INodeReferenceChecker} at selector execution time, and
*       allow the selection of nodes by features other than the standard ones (name, attributes, etc.).</li>
* </ul>
* <h3>Syntax</h3>
* <p>
*   DOM Selector syntax is similar to that of XPath or jQuery selectors:
* </p>
* <ul>
*   <li>Paths:
*     <ul>
*       <li><tt>/x</tt> means <i>direct children of the current node which either have name <tt>x</tt> or match
*           reference <tt>x</tt></i>. For example: <tt>/html/body/ul/li</tt>.</li>
*       <li><tt>//x</tt> means <i>children of the current node which either have name <tt>x</tt> or match
*           reference <tt>x</tt>, at any depth</i>. For example: <tt>//li</tt>.</li></li>
*       <li><tt>x</tt> is exactly equivalent to <tt>//x</tt>.</li>
*       <li><tt>text()</tt> means <i>Text nodes (at the specified level)</i>. For example: <tt>//li/text()</tt>.</li>
*     </ul>
*   </li>
*   <li>Attribute/index modifiers:
*     <ul>
*       <li><tt>x[@z='v']</tt> means <i>elements with name <tt>x</tt> and an attribute called z with
*           value <tt>v</tt></i>.</li>
*       <li><tt>[@z='v']</tt> means <i>elements with any name and an attribute called z with
*           value <tt>v</tt></i>.</li>
*       <li>Other operators are also valid, besides <tt>=</tt> (equal): <tt>!=</tt> (not equal),
*           <tt>^=</tt> (starts with) and <tt>$=</tt> (ends with). For example:
*           <tt>x[@class^='section']</tt> means <i>elements with name <tt>x</tt> and a value for
*           attribute <tt>class</tt> that starts with <tt>section</tt></i>.</li>
*       <li>Attributes can be specified both starting with <tt>@</tt> (XPath-style) and without
*           (jQuery-style). So <tt>x[@z='v']</tt> is actually equivalent to <tt>x[z='v']</tt>.</li>
*       <li><tt>x[@z1='v1' and @z2='v2']</tt> means <i>elements with name <tt>x</tt> and attributes
*           <tt>z1</tt> and <tt>z2</tt> with values <tt>v1</tt> and <tt>v2</tt>, respectively</i>.</li>
*       <li>Multiple-attribute modifiers can be joined with <tt>and</tt> (XPath-style) and also by chaining
*           multiple modifiers (jQuery-style). So <tt>x[@z1='v1' and @z2='v2']</tt> is actually equivalent
*           to <tt>x[@z1='v1'][@z2='v2']</tt> (and also to <tt>x[z1='v1'][z2='v2']</tt>)</li>
*       <li><tt>x[i]</tt> means <i>element with name <tt>x</tt> positioned in number <tt>i</tt> among
*           its siblings</i>. Note index modifiers must always come after all attribute modifiers.</li>
*       <li><tt>x[@z='v'][i]</tt> means <i>elements with name <tt>x</tt>, attribute <tt>z</tt> with
*           value <tt>v</tt> and positioned in number <tt>i</tt> among its siblings that also match this
*           condition</i>.</li>
*     </ul>
*   </li>
*   <li>Direct selectors:
*     <ul>
*       <li><tt>x.oneclass</tt> is equivalent to <tt>x[class='oneclass']</tt>.</li>
*       <li><tt>.oneclass</tt> is equivalent to <tt>[class='oneclass']</tt>.</li>
*       <li><tt>x#oneid</tt> is equivalent to <tt>x[id='oneid']</tt>.</li>
*       <li><tt>#oneid</tt> is equivalent to <tt>[id='oneid']</tt>.</li>
*       <li><tt>x%oneref</tt> means <i>nodes -not just elements- with name <tt>x</tt> that match reference <tt>oneref</tt> according to
*           the specified {@link INodeReferenceChecker} implementation.</i>.</li>
*       <li><tt>%oneref</tt> means <i>nodes -not just elements- with any name that match reference <tt>oneref</tt> according to
*           the specified {@link INodeReferenceChecker} implementation.</i>. Note this is actually equivalent
*           to simply <tt>oneref</tt> because references can be used instead of element names, as explained above.</li>
*       <li>Direct selectors and attribute selectors can be mixed: <tt>a.external[@href^='https']</tt>.</li>
*     </ul>
*   </li>
*   <li>Specific features:
*     <ul>
*       <li>DOM Selectors understand the <tt>class</tt> attribute to be multivalued, and therefore allow
*           the application of modifiers on this attribute even if the element has several class values. For example,
*           <tt>x[class='two']</tt> will match <tt>&lt;x class="one two three"/&gt;</tt>.</li>
*     </ul>
*   </li>
* </ul>
* <p>
*   Objects of this class are <b>thread-safe</b>.
* </p>
*
* @author Daniel Fern&aacute;ndez
*
* @since 2.0.0
*
*/
public final class DOMSelector implements Serializable {

    private static final long serialVersionUID = -1680336779267140369L;

    private static final String TEXT_SELECTOR = "text()";
    private static final String ID_MODIFIER_SEPARATOR = "#";
    private static final String CLASS_MODIFIER_SEPARATOR = ".";
    private static final String REFERENCE_MODIFIER_SEPARATOR = "%";

    private static final String ID_ATTRIBUTE_NAME = "id";
    private static final String CLASS_ATTRIBUTE_NAME = "class";

    private static final String selectorPatternStr = "^(/{1,2})([^/\\s]*?)(\\[(?:.*)\\])?$";
    private static final Pattern selectorPattern = Pattern.compile(selectorPatternStr);
    private static final String modifiersPatternStr = "^(?:\\[(.*?)\\])(\\[(?:.*)\\])?$";
    private static final Pattern modifiersPattern = Pattern.compile(modifiersPatternStr);

    private final String selectorExpression;
    private final boolean descendMoreThanOneLevel;

    private final String selectorPath; // This will not be normalized in case it is a reference
    private final String normalizedSelectorPath; // We keep a normalized version in case it refers to a tag name
    private final String selectorPathIdModifier;
    private final String selectorPathClassModifier;
    private final String selectorPathReferenceModifier;
    private final boolean text;
    private List<AttributeCondition> attributes = null;
    private Integer index = null; // will be -1 if last()

    private final DOMSelector next;



    /**
     * <p>
     *   Creates a new DOM selector specified by the argument selector
     *   expression.
     * </p>
     *
     * @param selectorExpression the expression specifying the selector to be used.
     */
    public DOMSelector(final String selectorExpression) {
        this(selectorExpression, true);
    }



    private DOMSelector(final String selectorExpression, final boolean atRootLevel) {

        super();

        /*
         * STRATEGY: We will divide the DOM Selector into several, one for each level, and chain them all using the
         * 'next' property. That way, a '/x//y[0]/z[@id='a']' selector will be divided into three chained selectors,
         * like: '/x' -(next)-> '//y[0]' -(next)-> '/z[@id='a']'
         */

        this.selectorExpression = selectorExpression;

        String selectorSpecStr = selectorExpression.trim();
        if (atRootLevel) {
            if (!selectorSpecStr.startsWith("/")) {
                // "x" is equivalent to "//x"
                selectorSpecStr = "//" + selectorSpecStr;
            }
        } // if we are not at root level, expression will always start with "/", and that's fine.

        final int selectorSpecStrLen = selectorSpecStr.length();
        int firstNonSlash = 0;
        while (firstNonSlash < selectorSpecStrLen && selectorSpecStr.charAt(firstNonSlash) == '/') {
            firstNonSlash++;
        }
       
        if (firstNonSlash >= selectorSpecStrLen) {
            throw new TemplateProcessingException(
                    "Invalid syntax in DOM selector \"" + selectorExpression + "\": '/' should be followed by " +
                    "further selector specification");
        }
       
        final int selEnd = selectorSpecStr.substring(firstNonSlash).indexOf('/');
        if (selEnd != -1) {
            final String tail = selectorSpecStr.substring(firstNonSlash).substring(selEnd);
            selectorSpecStr = selectorSpecStr.substring(0, firstNonSlash + selEnd);
            this.next = new DOMSelector(tail, false);
        } else {
            this.next = null;
        }

        final Matcher matcher = selectorPattern.matcher(selectorSpecStr);
        if (!matcher.matches()) {
            throw new TemplateProcessingException(
                    "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                "((/|//)?selector)?([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
        }
       
        final String rootGroup = matcher.group(1);
        final String selectorNameGroup = matcher.group(2);
        final String modifiersGroup = matcher.group(3);

        if (rootGroup == null) {
            throw new TemplateProcessingException(
                    "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                    "((/|//)?selector)?([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
        }
       
        if ("//".equals(rootGroup)) {
            this.descendMoreThanOneLevel = true;
        } else if ("/".equals(rootGroup)) {
            this.descendMoreThanOneLevel = false;
        } else {
            throw new TemplateProcessingException(
                    "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                    "((/|//)?selector)?([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
        }
       
        if (selectorNameGroup == null) {
            throw new TemplateProcessingException(
                    "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                    "((/|//)?selector)?([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
        }


        /*
         * Process path: extract id, class, reference modifiers...
         */

        String path = selectorNameGroup;

        final int idModifierPos = path.indexOf(ID_MODIFIER_SEPARATOR);
        final int classModifierPos = path.indexOf(CLASS_MODIFIER_SEPARATOR);
        final int referenceModifierPos = path.indexOf(REFERENCE_MODIFIER_SEPARATOR);

        if (idModifierPos != -1) {
            if (classModifierPos != -1 || referenceModifierPos != -1) {
                throw new TemplateProcessingException(
                        "More than one modifier (id, class, reference) have been specified at " +
                        "DOM selector expression \"" + this.selectorExpression + "\", which is forbidden.");
            }
            this.selectorPathIdModifier = path.substring(idModifierPos + ID_MODIFIER_SEPARATOR.length());
            path = path.substring(0, idModifierPos);
            if (StringUtils.isEmptyOrWhitespace(this.selectorPathIdModifier)) {
                throw new TemplateProcessingException(
                        "Empty id modifier in DOM selector expression " +
                        "\"" + this.selectorExpression + "\", which is forbidden.");
            }
        } else {
            this.selectorPathIdModifier = null;
        }

        if (classModifierPos != -1) {
            if (idModifierPos != -1 || referenceModifierPos != -1) {
                throw new TemplateProcessingException(
                        "More than one modifier (id, class, reference) have been specified at " +
                                "DOM selector expression \"" + this.selectorExpression + "\", which is forbidden.");
            }
            this.selectorPathClassModifier = path.substring(classModifierPos + CLASS_MODIFIER_SEPARATOR.length());
            path = path.substring(0, classModifierPos);
            if (StringUtils.isEmptyOrWhitespace(this.selectorPathClassModifier)) {
                throw new TemplateProcessingException(
                        "Empty id modifier in DOM selector expression " +
                                "\"" + this.selectorExpression + "\", which is forbidden.");
            }
        } else {
            this.selectorPathClassModifier = null;
        }

        if (referenceModifierPos != -1) {
            if (idModifierPos != -1 || classModifierPos != -1) {
                throw new TemplateProcessingException(
                        "More than one modifier (id, class, reference) have been specified at " +
                                "DOM selector expression \"" + this.selectorExpression + "\", which is forbidden.");
            }
            this.selectorPathReferenceModifier = path.substring(referenceModifierPos + REFERENCE_MODIFIER_SEPARATOR.length());
            path = path.substring(0, referenceModifierPos);
            if (StringUtils.isEmptyOrWhitespace(this.selectorPathReferenceModifier)) {
                throw new TemplateProcessingException(
                        "Empty id modifier in DOM selector expression " +
                                "\"" + this.selectorExpression + "\", which is forbidden.");
            }
        } else {
            this.selectorPathReferenceModifier = null;
        }

        this.selectorPath = path;
        // We use element normalization because path is made up of element names
        this.normalizedSelectorPath = Element.normalizeElementName(this.selectorPath);
        this.text = TEXT_SELECTOR.equals(this.normalizedSelectorPath);


        /*
         * Process classifiers: attributes and index.
         */

        if (modifiersGroup != null) {

            /*
             * A selector level can include two types of filters between [...], in this order:
             *   * 1. Attribute based: [@a='X' and @b='Y'], any number of them: [@a='X'][@b='Y']...
             *   * 2. Index based: [23]
             */

            String remainingModifiers = modifiersGroup;

            while (remainingModifiers != null) {

                // This pattern is made to be recursive, acting group 2 as the recursion tail
                final Matcher modifiersMatcher = modifiersPattern.matcher(remainingModifiers);
                if (!modifiersMatcher.matches()) {
                    throw new TemplateProcessingException(
                            "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                                    "((/|//)?selector)?([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
                }

                final String currentModifier = modifiersMatcher.group(1);
                remainingModifiers = modifiersMatcher.group(2);

                final Integer modifierAsIndex = parseIndex(currentModifier);

                if (modifierAsIndex != null) {

                    this.index = modifierAsIndex;
                    if (remainingModifiers != null) {
                        // If this is an index, it must be the last modifier!
                        throw new TemplateProcessingException(
                                "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                                        "((/|//)?selector)?([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
                    }

                } else {
                    // Modifier is not an index

                    final List<AttributeCondition> attribs = parseAttributes(selectorExpression, currentModifier);
                    if (attribs == null) {
                        throw new TemplateProcessingException(
                                "Invalid syntax in DOM selector \"" + selectorExpression + "\": selector does not match selector syntax: " +
                                        "(/|//)(selector)([@attrib=\"value\" (and @attrib2=\"value\")?])?([index])?");
                    }

                    if (this.attributes == null) {
                        // This is done to save an object. The method that creates the "attribs" list is completely
                        // under our control, so there should be no problem.
                        this.attributes = attribs;
                    } else {
                        this.attributes.addAll(attribs);
                    }

                }

            }

            if (this.descendMoreThanOneLevel && this.index != null) {
                throw new TemplateProcessingException(
                        "Invalid syntax in DOM selector \"" + selectorExpression + "\": index cannot be specified on a \"descend any levels\" selector (//).");
            }
           
        }
       
    }
   





   
    /**
     * <p>
     *   Returns the expression that specifies this DOM selector.
     * </p>
     *
     * @return the selector expression.
     * @since 2.0.12
     */
    public String getSelectorExpression() {
        return this.selectorExpression;
    }
   
   
   
    private static Integer parseIndex(final String indexGroup) {
        if ("last()".equals(indexGroup.toLowerCase())) {
            return Integer.valueOf(-1);
        }
        try {
            return Integer.valueOf(indexGroup);
        } catch (final Exception ignored) {
            return null;
        }
    }
   

   
    private static List<AttributeCondition> parseAttributes(final String selectorSpec, final String indexGroup) {
        final List<AttributeCondition> attributes = new ArrayList<AttributeCondition>(3);
        parseAttributes(selectorSpec, attributes, indexGroup);
        return attributes;
    }

   
    private static void parseAttributes(final String selectorSpec, final List<AttributeCondition> attributes, final String indexGroup) {
       
        String att = null;
        final int andPos = indexGroup.indexOf(" and ");
        if (andPos != -1) {
            att = indexGroup.substring(0,andPos);
            final String tail = indexGroup.substring(andPos + 5);
            parseAttributes(selectorSpec, attributes, tail);
        } else {
            att = indexGroup;
        }
           
        parseAttribute(selectorSpec, attributes, att);
       
    }

   
   
    private static void parseAttribute(final String selectorSpec, final List<AttributeCondition> attributes, final String attributeSpec) {

        // 0 = attribute name, 1 = operator, 2 = value
        final String[] fragments = AttributeCondition.Operator.extractOperator(attributeSpec);

        if (fragments[1] != null) {
            // There is an operator

            String attrName = fragments[0];
            final AttributeCondition.Operator operator = AttributeCondition.Operator.parse(fragments[1]);
            final String attrValue = fragments[2];
            if (attrName.startsWith("@")) {
                attrName = attrName.substring(1);
            }
            if (!(attrValue.startsWith("\"") && attrValue.endsWith("\"")) && !(attrValue.startsWith("'") && attrValue.endsWith("'"))) {
                throw new TemplateProcessingException(
                        "Invalid syntax in DOM selector: \"" + selectorSpec + "\"");
            }
            attributes.add(new AttributeCondition(Attribute.normalizeAttributeName(attrName), operator, attrValue.substring(1, attrValue.length() - 1)));

        } else {
            // There is NO operator

            String attrName = fragments[0];
            if (attrName.startsWith("@")) {
                attrName = attrName.substring(1);
            }
            attributes.add(new AttributeCondition(Attribute.normalizeAttributeName(attrName), null, null));

        }

    }



    /**
     * <p>
     *   Executes the DOM selector against the specified node, returning
     *   the result of applying the selector expression.
     * </p>
     *
     * @param node the node on which the selector will be executed.
     * @return the result of executing the selector.
     */
    public List<Node> select(final Node node) {
        Validate.notNull(node, "Node to be searched cannot be null");
        return select(Collections.singletonList(node), null);
    }


    /**
     * <p>
     *   Executes the DOM selector against the specified node and
     *   using the specified reference checker (if references are used in the DOM selector
     *   expression).
     * </p>
     *
     * @param node the node on which the selector will be executed.
     * @param referenceChecker the checker that will be used to compute whether a Node matches or not
     *        a specified reference. Can be null.
     * @return the result of executing the selector.
     * @since 2.1.0
     */
    public List<Node> select(final Node node, final INodeReferenceChecker referenceChecker) {
        Validate.notNull(node, "Node to be searched cannot be null");
        return select(Collections.singletonList(node), referenceChecker);
    }


    /**
     * <p>
     *   Executes the DOM selector against the specified list of nodes,
     *   returning the result of applying the selector expression.
     * </p>
     *
     * @param nodes the nodes on which the selector will be executed.
     * @return the result of executing the selector.
     */
    public List<Node> select(final List<Node> nodes) {
        return select(nodes, null);
    }


    /**
     * <p>
     *   Executes the DOM selector against the specified list of nodes and
     *   using the specified reference checker (if references are used in the DOM selector
     *   expression).
     * </p>
     *
     * @param nodes the nodes on which the selector will be executed.
     * @param referenceChecker the checker that will be used to compute whether a Node matches or not
     *        a specified reference. Can be null.
     * @return the result of executing the selector.
     * @since 2.1.0
     */
    public List<Node> select(final List<Node> nodes, final INodeReferenceChecker referenceChecker) {

        Validate.notEmpty(nodes, "Nodes to be searched cannot be null or empty");

        if (nodes.size() == 1 && nodes.get(0) instanceof Document) {
            final List<Node> selected = new ArrayList<Node>(10);
            for (final Node node : nodes) {
                doCheckNodeSelection(selected, node, referenceChecker);
            }
            return selected;
        }

        final List<List<Node>> selected = new ArrayList<List<Node>>(10);

        for (final Node node : nodes) {
            final List<Node> childSelectedNodes = new ArrayList<Node>(10);
            if (doCheckNodeSelection(childSelectedNodes, node, referenceChecker)) {
                selected.add(childSelectedNodes);
            }
        }

        if (selected.size() == 0) {
            return Collections.emptyList();
        }

        final List<Node> selectedNodes = new ArrayList<Node>(10);

        if (this.index == null) {
            for (final List<Node> selectedNodesForChild : selected) {
                selectedNodes.addAll(selectedNodesForChild);
            }
            return selectedNodes;
        }

        // There is an index

        if (this.index.intValue() == -1) {
            selectedNodes.addAll(selected.get(selected.size() - 1));
            return selectedNodes;
        }
        if (this.index.intValue() >= selected.size()) {
            return Collections.emptyList();
        }
        selectedNodes.addAll(selected.get(this.index.intValue()));
        return selectedNodes;

    }




   
    private boolean checkChildrenSelection(final List<Node> selectedNodes,
            final Node node, final INodeReferenceChecker referenceChecker) {
        // will return true if any nodes are added to selectedNodes

        if (node instanceof NestableNode) {

            final List<List<Node>> selectedNodesForChildren = new ArrayList<List<Node>>(10);

            final NestableNode nestableNode = (NestableNode) node;
            if (nestableNode.hasChildren()) {
                for (final Node child : nestableNode.getChildren()) {
                    final List<Node> childSelectedNodes = new ArrayList<Node>(10);
                    if (doCheckNodeSelection(childSelectedNodes, child, referenceChecker)) {
                        selectedNodesForChildren.add(childSelectedNodes);
                    }
                }
            }

            if (selectedNodesForChildren.size() == 0) {
                return false;
            }

            if (this.index == null) {
                for (final List<Node> selectedNodesForChild : selectedNodesForChildren) {
                    selectedNodes.addAll(selectedNodesForChild);
                }
                return true;
            }

            // There is an index

            if (this.index.intValue() == -1) {
                selectedNodes.addAll(selectedNodesForChildren.get(selectedNodesForChildren.size() - 1));
                return true;
            }
            if (this.index.intValue() >= selectedNodesForChildren.size()) {
                return false;
            }
            selectedNodes.addAll(selectedNodesForChildren.get(this.index.intValue()));
            return true;

        }

        return false;

    }
   
   
   
   
    private boolean doCheckNodeSelection(final List<Node> selectedNodes,
            final Node node, final INodeReferenceChecker referenceChecker) {

        if (!doCheckSpecificNodeSelection(node, referenceChecker)) {
           
            if (this.descendMoreThanOneLevel || node instanceof Document || node instanceof GroupNode) {
                // This level doesn't match, but maybe next levels do...
               
                if (node instanceof NestableNode) {
                   
                    final NestableNode nestableNode = (NestableNode) node;
                    if (nestableNode.hasChildren()) {
                        return checkChildrenSelection(selectedNodes, node, referenceChecker);
                    }
                   
                }
               
            }
           
            return false;
           
        }
       
        if (this.next == null) {
            selectedNodes.add(node);
            return true;
        }
       
        if (node instanceof NestableNode) {
           
            final NestableNode nestableNode = (NestableNode) node;
            if (nestableNode.hasChildren()) {
                if (this.next.checkChildrenSelection(selectedNodes, node, referenceChecker)) {
                    // This step of the selector matched, and we will try to match the next ones.
                    return true;
                }
                // This step matched, but not the next ones. So we might try again matching this same step
                // (not the "next" one) with our children
                return checkChildrenSelection(selectedNodes, node, referenceChecker);
            }
           
        }
       
        return false;
       
    }
   
    private boolean doCheckSpecificNodeSelection(final Node node, final INodeReferenceChecker referenceChecker) {
       
        // This method checks all aspects except index (index can only
        // be applied from the superior level)

        if (node instanceof Element) {

            final Element element = (Element)node;

            if (this.selectorPathIdModifier != null) {
                if (!checkPathWithIdModifier(element)) {
                    return false;
                }
            } else if (this.selectorPathClassModifier != null) {
                if (!checkPathWithClassModifier(element)) {
                    return false;
                }
            } else if (this.selectorPathReferenceModifier != null) {
                if (!checkPathWithReferenceModifier(element, referenceChecker)) {
                    return false;
                }
            } else {
                if (!checkPathWithoutModifiers(element, referenceChecker)) {
                    return false;
                }
            }

            if (this.attributes == null || this.attributes.size() == 0) {
                return true;
            }

            for (final AttributeCondition attributeCondition : this.attributes) {
                final String selectedAttributeName = attributeCondition.getName();
                final boolean selectedAttributeMultipe = CLASS_ATTRIBUTE_NAME.equals(selectedAttributeName);
                if (!checkAttributeValue(element, selectedAttributeName,
                                         attributeCondition.getOperator(), attributeCondition.getValue(),
                                         selectedAttributeMultipe)) {
                    return false;
                }
            }

            return true;

        }
        if (node instanceof AbstractTextNode) {
            if (referenceChecker != null) {
                return this.text &&
                       (this.selectorPathReferenceModifier == null || referenceChecker.checkReference(node, this.selectorPathReferenceModifier));
            }
            return this.text;
        }

        return false;
       
    }




    private static boolean checkAttributeValue(final NestableAttributeHolderNode node,
            final String attributeName, final AttributeCondition.Operator operator, final String attributeValue,
            final boolean multivalued) {

        if (!node.hasNormalizedAttribute(attributeName)) {
            if (attributeValue == null) {
                return operator == AttributeCondition.Operator.EQUALS;
            }
            return operator == AttributeCondition.Operator.NOT_EQUALS;
        }

        final String nodeAttributeValue = node.getAttributeValueFromNormalizedName(attributeName);

        if (nodeAttributeValue == null) {
            if (attributeValue == null) {
                return operator == AttributeCondition.Operator.EQUALS;
            }
            return operator == AttributeCondition.Operator.NOT_EQUALS;
        }
        if (attributeValue == null) {
            return operator == AttributeCondition.Operator.NOT_EQUALS;
        }

        if (!multivalued) {

            switch (operator) {
                case EQUALS:
                    return nodeAttributeValue.equals(attributeValue);
                case NOT_EQUALS:
                    return !nodeAttributeValue.equals(attributeValue);
                case STARTS_WITH:
                    return nodeAttributeValue.startsWith(attributeValue);
                case ENDS_WITH:
                    return nodeAttributeValue.endsWith(attributeValue);
            }

        }

        // Attribute IS multivalued

        if ((operator == AttributeCondition.Operator.EQUALS || operator == AttributeCondition.Operator.NOT_EQUALS)
                && !nodeAttributeValue.contains(attributeValue)) {
            // If it is equals/not equals, value must appear as a whole (not a prefix, suffix). If not, we can return.
            return operator == AttributeCondition.Operator.NOT_EQUALS;
        }

        final StringTokenizer nodeAttrValueTokenizer = new StringTokenizer(nodeAttributeValue, ", ");
        while (nodeAttrValueTokenizer.hasMoreTokens()) {
            final String nodeAttrValueToken = nodeAttrValueTokenizer.nextToken();
            switch (operator) {
                case EQUALS:
                    if (nodeAttrValueToken.equals(attributeValue)) {
                        return true;
                    }
                    break;
                case NOT_EQUALS:
                    if (!nodeAttrValueToken.equals(attributeValue)) {
                        return true;
                    }
                    break;
                case STARTS_WITH:
                    if (nodeAttrValueToken.startsWith(attributeValue)) {
                        return true;
                    }
                    break;
                case ENDS_WITH:
                    if (nodeAttrValueToken.endsWith(attributeValue)) {
                        return true;
                    }
                    break;
            }
        }

        return false;

    }



    private boolean checkPathWithIdModifier(final Element element) {

        if (this.selectorPathIdModifier == null) {
            return false;
        }

        final String elementName = element.getNormalizedName();

        if (!StringUtils.isEmptyOrWhitespace(this.normalizedSelectorPath)) {
            if (!this.normalizedSelectorPath.equals(elementName)) {
                return false;
            }

        }
        // Checking the element name went OK, so lets check the ID
        return checkAttributeValue(element, ID_ATTRIBUTE_NAME, AttributeCondition.Operator.EQUALS, this.selectorPathIdModifier, false);

    }


    private boolean checkPathWithClassModifier(final Element element) {

        if (this.selectorPathClassModifier == null) {
            return false;
        }

        final String elementName = element.getNormalizedName();

        if (!StringUtils.isEmptyOrWhitespace(this.normalizedSelectorPath)) {
            if (!this.normalizedSelectorPath.equals(elementName)) {
                return false;
            }

        }
        // Checking the element name went OK, so lets check the class
        return checkAttributeValue(element, CLASS_ATTRIBUTE_NAME, AttributeCondition.Operator.EQUALS, this.selectorPathClassModifier, true);

    }


    private boolean checkPathWithReferenceModifier(final Element element, final INodeReferenceChecker referenceChecker) {

        if (this.selectorPathReferenceModifier == null || referenceChecker == null) {
            // First one being null never happen, as we should never call this method if modifier is null
            return false;
        }

        final String elementName = element.getNormalizedName();

        if (!StringUtils.isEmptyOrWhitespace(this.normalizedSelectorPath)) {
            if (!this.normalizedSelectorPath.equals(elementName)) {
                return false;
            }

        }
        // Checking the element name went OK, so lets check the reference
        return referenceChecker.checkReference(element, this.selectorPathReferenceModifier);

    }


    private boolean checkPathWithoutModifiers(final Element element, final INodeReferenceChecker referenceChecker) {

        final String elementName = element.getNormalizedName();
        if (!StringUtils.isEmptyOrWhitespace(this.selectorPath)) {
            if (!this.normalizedSelectorPath.equals(elementName)) {
                if (referenceChecker == null) {
                    return false;
                }
                return referenceChecker.checkReference(element, this.selectorPath); // This is the NOT normalized one!
            }
        }
        // We don't have any reasons to deny it matches
        return true;

    }


   
   
    @Override
    public String toString() {
        return this.selectorExpression;
    }





    private static final class AttributeCondition {

        static enum Operator {
                EQUALS, NOT_EQUALS, STARTS_WITH, ENDS_WITH;

                static Operator parse(final String operatorStr) {
                    if (operatorStr == null) {
                        return null;
                    }
                    if ("=".equals(operatorStr)) {
                        return EQUALS;
                    }
                    if ("!=".equals(operatorStr)) {
                        return NOT_EQUALS;
                    }
                    if ("^=".equals(operatorStr)) {
                        return STARTS_WITH;
                    }
                    if ("$=".equals(operatorStr)) {
                        return ENDS_WITH;
                    }
                    return null;
                }

                static String[] extractOperator(final String specification) {
                    final int equalsPos = specification.indexOf('=');
                    if (equalsPos == -1) {
                        return new String[] {specification.trim(), null, null};
                    }
                    final char cprev = specification.charAt(equalsPos - 1);
                    switch (cprev) {
                        case '!':
                            return new String[] {
                                    specification.substring(0, equalsPos - 1).trim(), "!=",
                                    specification.substring(equalsPos + 1).trim()};
                        case '^':
                            return new String[] {
                                    specification.substring(0, equalsPos - 1).trim(), "^=",
                                    specification.substring(equalsPos + 1).trim()};
                        case '$':
                            return new String[] {
                                    specification.substring(0, equalsPos - 1).trim(), "$=",
                                    specification.substring(equalsPos + 1).trim()};
                        default:
                            return new String[] {
                                    specification.substring(0, equalsPos).trim(), "=",
                                    specification.substring(equalsPos + 1).trim()};
                    }
                }

            }


        private final String name;
        private final Operator operator;
        private final String value;

        AttributeCondition(final String name, final Operator operator, final String value) {
            super();
            this.name = name;
            this.operator = operator;
            this.value = value;
        }

        String getName() {
            return this.name;
        }

        Operator getOperator() {
            return this.operator;
        }

        String getValue() {
            return this.value;
        }

    }


    /**
     * <p>
     *     Common interface for objects in charge of resolving references in DOM Selector
     *     expressions.
     * </p>
     * <p>
     *     These objects will be used in DOM Selectors for determining how references (like <tt>%something</tt>)
     *     will be resolved. The most common reference checker implementation is
     *     {@link org.thymeleaf.standard.fragment.StandardFragmentSignatureNodeReferenceChecker}, typically used
     *     for looking for <tt>th:fragment</tt> attributes in templates.
     * </p>
     *
     * @author Daniel Fern&aacute;ndez
     *
     * @since 2.1.0
     */
    public static interface INodeReferenceChecker {

        /**
         * <p>
         *     Check whether the node passed as argument matches the specified reference value.
         * </p>
         *
         * @param node the node to be checked.
         * @param referenceValue the reference value expected to check.
         * @return true if the node matches the reference value, false if not.
         */
        public boolean checkReference(final Node node, final String referenceValue);

    }


    /**
     * <p>
     *     Common abstract implementation of {@link INodeReferenceChecker}.
     * </p>
     *
     * @author Daniel Fern&aacute;ndez
     *
     * @since 2.1.0
     */
    public abstract static class AbstractNodeReferenceChecker implements INodeReferenceChecker {

        protected AbstractNodeReferenceChecker() {
            super();
        }

    }


    /**
     * <p>
     *     Implementation of {@link INodeReferenceChecker} that aggregates two other reference
     *     checker objects.
     * </p>
     * <p>
     *     First, <tt>one</tt> is checked and, if false is returned, <tt>two</tt> is checked.
     * </p>
     *
     * @author Daniel Fern&aacute;ndez
     *
     * @since 2.1.0
     */
    public static final class AggregatingNodeReferenceChecker extends AbstractNodeReferenceChecker {

        private final INodeReferenceChecker one;
        private final INodeReferenceChecker two;

        public AggregatingNodeReferenceChecker(final INodeReferenceChecker one, final INodeReferenceChecker two) {
            super();
            Validate.notNull(one, "Reference checker one cannot be null");
            Validate.notNull(two, "Reference checker two cannot be null");
            this.one = one;
            this.two = two;
        }

        public boolean checkReference(final Node node, final String referenceValue) {
            if (this.one.checkReference(node, referenceValue)) {
                return true;
            }
            return this.two.checkReference(node, referenceValue);
        }

    }

}
TOP

Related Classes of org.thymeleaf.dom.DOMSelector$INodeReferenceChecker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.