org.thymeleaf.dom.NestableNode Maven / Gradle / Ivy
Show all versions of thymeleaf Show documentation
/*
* =============================================================================
*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.thymeleaf.Arguments;
import org.thymeleaf.Configuration;
import org.thymeleaf.util.ArrayUtils;
import org.thymeleaf.util.IdentityCounter;
import org.thymeleaf.util.Validate;
/**
*
* Base abstract class for all nodes in a Thymeleaf DOM tree which have
* children.
*
*
* @author Daniel Fernández
*
* @since 2.0.0
*
*/
@SuppressWarnings("ObjectEquality")
public abstract class NestableNode extends Node {
private static final long serialVersionUID = -5601217853971985055L;
private static final int DEFAULT_CHILDREN_SIZE = 3;
private Node[] children = null;
private int childrenLen = 0;
private int childrenVersion = 0;
NestableNode(final String documentName, final Integer lineNumber) {
super(documentName, lineNumber);
}
/*
* ************************
* ************************
* CHILDREN
* ************************
* ************************
*/
/**
*
* Returns whether this node has any children.
*
*
* @return true if the node as any children, false if not.
*/
public final boolean hasChildren() {
return this.childrenLen != 0;
}
/**
*
* Returns the number of children in this node.
*
*
* @return the number of children.
*/
public final int numChildren() {
return this.childrenLen;
}
/**
*
* Returns the children of this node.
* The returned list is immutable.
*
*
* @return the list of children.
*/
public final List getChildren() {
if (this.childrenLen == 0) {
return Collections.emptyList();
}
return Arrays.asList(ArrayUtils.copyOf(this.children, this.childrenLen));
}
/**
*
* Returns the only the {@link Element} children
* of this node, discarding children of any other types.
* The returned list is immutable.
*
*
* @return the list of Element children.
*/
public final List getElementChildren() {
if (this.childrenLen == 0) {
return Collections.emptyList();
}
final List elementChildren = new ArrayList(this.childrenLen + 2);
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] instanceof Element) {
elementChildren.add((Element)this.children[i]);
}
}
return Collections.unmodifiableList(elementChildren);
}
/**
*
* Returns the real, unsafe, inner array of node children. DO NOT
* use this method directly. Modifying this array could result in
* severe DOM corruption.
*
*
* @return the array of node children.
*/
public final Node[] unsafeGetChildrenNodeArray() {
return this.children;
}
/**
*
* Returns the first child of this node.
*
*
* @return the first child.
*/
public final Node getFirstChild() {
if (this.childrenLen == 0) {
return null;
}
return this.children[0];
}
/**
*
* Returns the first child of type {@link Element}.
*
*
* @return the first Element child.
*/
public final Element getFirstElementChild() {
if (this.childrenLen == 0) {
return null;
}
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] instanceof Element) {
return (Element)this.children[i];
}
}
return null;
}
/**
*
* Adds a new child to the node.
*
*
* @param newChild the new child to be added.
*/
public void addChild(final Node newChild) {
if (newChild != null) {
if (this.childrenLen == 0) {
this.children = new Node[DEFAULT_CHILDREN_SIZE];
this.children[0] = newChild;
this.childrenLen = 1;
this.childrenVersion++;
} else {
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] == newChild) {
return;
}
}
if (this.childrenLen >= this.children.length) {
this.children = ArrayUtils.copyOf(this.children, this.children.length * 2);
}
this.children[this.childrenLen++] = newChild;
this.childrenVersion++;
}
newChild.parent = this;
if (getProcessTextNodes()) {
// If we are already processing text nodes, we must make sure the new child also does
newChild.setProcessTextNodes(getProcessTextNodes());
}
if (getProcessCommentNodes()) {
// If we are already processing comment nodes, we must make sure the new child also does
newChild.setProcessCommentNodes(getProcessCommentNodes());
}
}
}
/**
*
* Adds a new child to the node, at a specific position.
*
*
* All children nodes from that position are moved one position
* forward in order to make room for the new child.
*
*
* @param index the position to insert the new child into.
* @param newChild the new child.
*/
public final void insertChild(final int index, final Node newChild) {
Validate.isTrue(index >= 0, "Index for inserting child must be >= 0");
Validate.isTrue(index <= this.childrenLen, "Index for inserting child must be less or equal than size (" + this.childrenLen + ")");
if (newChild != null) {
if (this.childrenLen > 0) {
/*
* If new child is already there, remove it so that it can be
* added in its new position.
*/
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] == newChild) {
if (i == index) {
return;
}
unsafeRemoveChild(i);
break;
}
}
// childrenLen could have changed, so this should be validated again
Validate.isTrue(index <= this.childrenLen, "Index for inserting child must be less or equal than size (" + this.childrenLen + ")");
}
if (this.childrenLen == 0) {
this.children = new Node[DEFAULT_CHILDREN_SIZE];
this.children[0] = newChild;
this.childrenLen = 1;
this.childrenVersion++;
} else {
if (this.childrenLen >= this.children.length) {
this.children = ArrayUtils.copyOf(this.children, this.children.length * 2);
}
System.arraycopy(this.children, index, this.children, index + 1, (this.childrenLen - index));
this.children[index] = newChild;
this.childrenLen++;
this.childrenVersion++;
}
newChild.parent = this;
if (getProcessTextNodes()) {
// If we are already processing text nodes, we must make sure the new child also does
newChild.setProcessTextNodes(getProcessTextNodes());
}
if (getProcessCommentNodes()) {
// If we are already processing comment nodes, we must make sure the new child also does
newChild.setProcessCommentNodes(getProcessCommentNodes());
}
}
}
/**
*
* Adds a new children to the node, positioned just before
* another child node that is also specified.
*
*
* This method is effectively equivalent to first searching
* the existing child and then executing {@link #insertChild(int, Node)}
* specifying its position.
*
*
* @param existingChild the child we want to insert the new child just before.
* @param newChild the new child.
*/
public final void insertBefore(final Node existingChild, final Node newChild) {
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] == existingChild) {
insertChild(i, newChild);
return;
}
}
throw new IllegalArgumentException("Child does not exist: cannot execute 'insertBefore' operation");
}
/**
*
* Adds a new children to the node, positioned just after
* another child node that is also specified.
*
*
* This method is effectively equivalent to first searching
* the existing child and then executing {@link #insertChild(int, Node)}
* specifying its position + 1.
*
*
* @param existingChild the child we want to insert the new child just after.
* @param newChild the new child.
*/
public final void insertAfter(final Node existingChild, final Node newChild) {
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] == existingChild) {
insertChild(i + 1, newChild);
return;
}
}
throw new IllegalArgumentException("Child does not exist: cannot execute 'insertAfter' operation");
}
/**
*
* Sets the new children of the node to the specified list.
*
*
* @param newChildren the new chidren.
*/
public final void setChildren(final List newChildren) {
if (this.children != null) {
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].parent = null;
}
this.children = null;
this.childrenLen = 0;
}
if (newChildren == null || newChildren.size() == 0) {
this.children = null;
this.childrenLen = 0;
this.childrenVersion++;
} else {
for (final Node newChild : newChildren) {
// Version will be changed inside addChild()
addChild(newChild);
}
}
}
/**
*
* Removes all the children nodes.
*
*/
public final void clearChildren() {
if (this.children != null) {
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].parent = null;
}
this.children = null;
this.childrenLen = 0;
this.childrenVersion++;
}
}
/**
*
* Removes a node child at a specific position.
*
*
* @param index the position to be removed.
*/
public final void removeChild(final int index) {
Validate.isTrue(index >= 0, "Index of child to remove must be >= 0");
Validate.isTrue(index < this.childrenLen, "Index of child to be removed must be less than size (" + this.childrenLen + ")");
unsafeRemoveChild(index);
}
final void unsafeRemoveChild(final int index) {
this.children[index].parent = null;
System.arraycopy(this.children, index + 1, this.children, index, (this.childrenLen - (index + 1)));
this.childrenLen--;
this.childrenVersion++;
}
/**
*
* Removes a specific child node from this node.
*
*
* @param child the child to be removed.
*/
public final void removeChild(final Node child) {
Validate.notNull(child, "Child cannot be null");
unsafeRemoveChild(child);
}
final void unsafeRemoveChild(final Node child) {
if (this.childrenLen > 0) {
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] == child) {
unsafeRemoveChild(i);
return;
}
}
}
}
/**
*
* Refactors a DOM tree by moving all the children of this node
* to another (which will be their new parent node).
*
*
* @param newParent the new parent.
*/
public final void moveAllChildren(final NestableNode newParent) {
Validate.notNull(newParent, "New parent cannot be null");
if (this.childrenLen > 0) {
for (int i = 0; i < this.childrenLen; i++) {
newParent.addChild(this.children[i]);
}
this.children = null;
this.childrenLen = 0;
this.childrenVersion++;
}
}
/**
*
* Extracts a child by removing it from the DOM tree and lifting all
* of its children one level, so that they become children nodes of
* this node.
*
*
* Node local variables, because of their hierarchical nature, are
* handled accordingly.
*
*
* @param child the child to be extracted.
*/
public final void extractChild(final Node child) {
if (child != null) {
if (child instanceof NestableNode) {
final NestableNode nestableChild = (NestableNode) child;
final NodeLocalVariablesMap nestableChildNodeLocalVariables =
nestableChild.unsafeGetNodeLocalVariables();
for (int i = 0; i < this.childrenLen; i++) {
if (this.children[i] == nestableChild) {
unsafeRemoveChild(i); // This will change childrenVersion
for (int j = 0; j < nestableChild.childrenLen; j++) {
insertChild(i + j, nestableChild.children[j]); // Additional changes to childrenVersion
nestableChild.children[j].addAllNonExistingNodeLocalVariables(nestableChildNodeLocalVariables);
}
return;
}
}
} else {
unsafeRemoveChild(child); // changed childrenVersion here too
}
}
}
/*
* ------------
* PRE-PROCESSING AND EXECUTION
* ------------
*/
@Override
final void doAdditionalPrecomputeNode(final Configuration configuration) {
/*
* Precompute children
*/
if (this.childrenLen > 0) {
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].precomputeNode(configuration);
}
}
doAdditionalPrecomputeNestableNode(configuration);
}
abstract void doAdditionalPrecomputeNestableNode(final Configuration configuration);
/*
* -------------------------------
* SKIPPABILITY AND PROCESSABILITY
* -------------------------------
*/
@Override
final void doAdditionalSkippableComputing(final boolean skippable) {
if (skippable && this.childrenLen > 0) {
// If this node is marked as skippable, all of its
// children should be marked skippable too.
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].setSkippable(true);
}
}
}
@Override
final void doAdditionalProcessableComputing(final boolean processable) {
if (!processable && this.childrenLen > 0) {
// If this node is marked as non-processable, all of its
// children should be marked non-processable too.
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].setProcessable(false);
}
}
}
@Override
public void setProcessTextNodes(final boolean processTextNodes) {
super.setProcessTextNodes(processTextNodes);
if (this.childrenLen > 0) {
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].setProcessTextNodes(processTextNodes);
}
}
}
@Override
public void setProcessCommentNodes(final boolean processCommentNodes) {
super.setProcessCommentNodes(processCommentNodes);
if (this.childrenLen > 0) {
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].setProcessCommentNodes(processCommentNodes);
}
}
}
/*
* ------------
* CLONING
* ------------
*/
@Override
final void doCloneNodeInternals(final Node node, final NestableNode newParent, final boolean cloneProcessors) {
final NestableNode nestableNode = (NestableNode) node;
if (this.childrenLen > 0) {
final Node[] elementChildren = new Node[this.childrenLen];
for (int i = 0; i < this.childrenLen; i++) {
elementChildren[i] = this.children[i].cloneNode(nestableNode, cloneProcessors);
}
nestableNode.children = elementChildren;
nestableNode.childrenLen = elementChildren.length;
nestableNode.childrenVersion++;
}
doCloneNestableNodeInternals(nestableNode, newParent, cloneProcessors);
}
abstract void doCloneNestableNodeInternals(final NestableNode node, final NestableNode newParent, final boolean cloneProcessors);
/*
* ------------
* PROCESSING
* ------------
*/
@Override
final void doAdditionalProcess(final Arguments arguments) {
if (this.childrenLen > 0) {
final IdentityCounter alreadyProcessed = new IdentityCounter(this.childrenLen + 3);
int currentChildrenVersion = this.childrenVersion;
int currentChildIndex = 0;
while (!isDetached() && computeNextChild(arguments, this, currentChildIndex, alreadyProcessed)) {
// By checking whether this node's children have actually been modified or not, we avoid having
// to continuously re-iterate over already-processed nodes at the 'computeNextChild()' method.
if (this.childrenVersion == currentChildrenVersion) {
currentChildIndex++;
} else {
currentChildrenVersion = this.childrenVersion;
currentChildIndex = 0;
}
}
}
}
private static boolean computeNextChild(
final Arguments arguments, final NestableNode node,
final int currentChildIndex, final IdentityCounter alreadyProcessed) {
// This method scans the whole array of children each time
// it tries to execute one so that it executes all sister nodes
// that might be created by, for example, iteration processors.
if (node.childrenLen > currentChildIndex) {
for (int i = currentChildIndex; i < node.childrenLen; i++) {
final Node child = node.children[i];
if (!alreadyProcessed.isAlreadyCounted(child)) {
child.processNode(arguments);
alreadyProcessed.count(child);
return true;
}
}
}
return false;
}
@Override
public final void visit(final DOMVisitor visitor) {
visitor.visit(this);
if (this.childrenLen > 0) {
for (int i = 0; i < this.childrenLen; i++) {
this.children[i].visit(visitor);
}
}
}
}