/*
* Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
*
* 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 freemarker.core;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateException;
import freemarker.template.TemplateNodeModel;
import freemarker.template.TemplateSequenceModel;
/**
* <b>Internal API - subject to change:</b> Represent directive call, interpolation, text block, or other such
* non-expression node in the parsed template. Some information that can be found here can be accessed through the
* {@link Environment#getCurrentDirectiveCallPlace()}, which a published API, and thus promises backward
* compatibility.
*
* @deprecated This is an internal FreeMarker API with no backward compatibility guarantees, so you shouldn't depend on
* it.
*/
@Deprecated
abstract public class TemplateElement extends TemplateObject {
private static final int INITIAL_REGULATED_CHILD_BUFFER_CAPACITY = 6;
private TemplateElement parent;
/**
* Used by elements that has no fixed schema for its child elements. For example, a {@code #case} can enclose any
* kind of elements. Only one of {@link #nestedBlock} and {@link #regulatedChildBuffer} can be non-{@code null}.
* This element is typically a {@link MixedContent}, at least before {@link #postParseCleanup(boolean)} (which
* optimizes out {@link MixedContent} with child count less than 2).
*/
private TemplateElement nestedBlock;
/**
* Used by elements that has a fixed schema for its child elements. For example, {@code #switch} can only have
* {@code #case} and {@code #default} child elements. Only one of {@link #nestedBlock} and
* {@link #regulatedChildBuffer} can be non-{@code null}.
*/
private TemplateElement[] regulatedChildBuffer;
private int regulatedChildCount;
/**
* The index of the element in the parent's {@link #regulatedChildBuffer} array, or 0 if this is the
* {@link #nestedBlock} of the parent.
*
* @since 2.3.23
*/
private int index;
/**
* Processes the contents of this <tt>TemplateElement</tt> and
* outputs the resulting text
*
* @param env The runtime environment
*/
abstract void accept(Environment env) throws TemplateException, IOException;
/**
* One-line description of the element, that contain all the information that is used in
* {@link #getCanonicalForm()}, except the nested content (elements) of the element. The expressions inside the
* element (the parameters) has to be shown. Meant to be used for stack traces, also for tree views that don't go
* down to the expression-level. There are no backward-compatibility guarantees regarding the format used ATM, but
* it must be regular enough to be machine-parseable, and it must contain all information necessary for restoring an
* AST equivalent to the original.
*
* This final implementation calls {@link #dump(boolean) dump(false)}.
*
* @see #getCanonicalForm()
* @see #getNodeTypeSymbol()
*/
public final String getDescription() {
return dump(false);
}
/**
* This final implementation calls {@link #dump(boolean) dump(false)}.
*/
@Override
public final String getCanonicalForm() {
return dump(true);
}
/**
* Tells if the element should show up in error stack traces. If you think you need to set this to {@code false} for
* an element, always consider if you should use {@link Environment#visitByHiddingParent(TemplateElement)} instead.
*
* Note that this will be ignored for the top (current) element of a stack trace, as that's always shown.
*/
boolean isShownInStackTrace() {
return true;
}
/**
* Tells if this element possibly executes its {@link #nestedBlock} for many times. This flag is useful when
* a template AST is modified for running time limiting (see {@link ThreadInterruptionSupportTemplatePostProcessor}).
* Elements that use {@link #regulatedChildBuffer} should not need this, as the insertion of the timeout checks is
* impossible there, given their rigid nested element schema.
*/
abstract boolean isNestedBlockRepeater();
/**
* Brings the implementation of {@link #getCanonicalForm()} and {@link #getDescription()} to a single place.
* Don't call those methods in method on {@code this}, because that will result in infinite recursion!
*
* @param canonical if {@code true}, it calculates the return value of {@link #getCanonicalForm()},
* otherwise of {@link #getDescription()}.
*/
abstract protected String dump(boolean canonical);
// Methods to implement TemplateNodeModel
public TemplateNodeModel getParentNode() {
// return parent;
return null;
}
public String getNodeNamespace() {
return null;
}
public String getNodeType() {
return "element";
}
public TemplateSequenceModel getChildNodes() {
if (regulatedChildBuffer != null) {
final SimpleSequence seq = new SimpleSequence(regulatedChildCount);
for (int i = 0; i < regulatedChildCount; i++) {
seq.add(regulatedChildBuffer[i]);
}
return seq;
}
SimpleSequence result = new SimpleSequence(1);
if (nestedBlock != null) {
result.add(nestedBlock);
}
return result;
}
public String getNodeName() {
String classname = this.getClass().getName();
int shortNameOffset = classname.lastIndexOf('.') + 1;
return classname.substring(shortNameOffset);
}
// Methods so that we can implement the Swing TreeNode API.
public boolean isLeaf() {
return nestedBlock == null && regulatedChildCount == 0;
}
/**
* @deprecated Meaningless; simply returns if the node currently has any child nodes.
*/
@Deprecated
public boolean getAllowsChildren() {
return !isLeaf();
}
public int getIndex(TemplateElement node) {
if (nestedBlock instanceof MixedContent) {
return nestedBlock.getIndex(node);
}
if (nestedBlock != null) {
if (node == nestedBlock) {
return 0;
}
} else {
for (int i = 0; i < regulatedChildCount; i++) {
if (regulatedChildBuffer[i].equals(node)) {
return i;
}
}
}
return -1;
}
public int getChildCount() {
if (nestedBlock instanceof MixedContent) {
return nestedBlock.getChildCount();
}
if (nestedBlock != null) {
return 1;
}
return regulatedChildCount;
}
/**
* Note: For element with {@code #nestedBlock}, this will hide the {@code #nestedBlock} when that's a
* {@link MixedContent}.
*/
public Enumeration children() {
if (nestedBlock instanceof MixedContent) {
return nestedBlock.children();
}
if (nestedBlock != null) {
return Collections.enumeration(Collections.singletonList(nestedBlock));
} else if (regulatedChildBuffer != null) {
return new _ArrayEnumeration(regulatedChildBuffer, regulatedChildCount);
}
return Collections.enumeration(Collections.EMPTY_LIST);
}
public TemplateElement getChildAt(int index) {
if (nestedBlock instanceof MixedContent) {
return nestedBlock.getChildAt(index);
}
if (nestedBlock != null) {
if (index == 0) {
return nestedBlock;
}
throw new ArrayIndexOutOfBoundsException("invalid index");
} else if (regulatedChildCount != 0) {
try {
return regulatedChildBuffer[index];
} catch (ArrayIndexOutOfBoundsException e) {
// nestedElements was a List earlier, so we emulate the same kind of exception
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + regulatedChildCount);
}
}
throw new ArrayIndexOutOfBoundsException("Template element has no children");
}
public void setChildAt(int index, TemplateElement element) {
if (nestedBlock instanceof MixedContent) {
nestedBlock.setChildAt(index, element);
} else if (nestedBlock != null) {
if (index == 0) {
nestedBlock = element;
element.index = 0;
element.parent = this;
} else {
throw new IndexOutOfBoundsException("invalid index");
}
} else if (regulatedChildBuffer != null) {
regulatedChildBuffer[index] = element;
element.index = index;
element.parent = this;
} else {
throw new IndexOutOfBoundsException("element has no children");
}
}
/**
* The element whose child this element is, or {@code null} if this is the root node.
*
* @deprecated Don't use in internal code either; use {@link #getParentElement()} there.
*/
@Deprecated
public TemplateElement getParent() {
return parent;
}
final void setRegulatedChildBufferCapacity(int capacity) {
int ln = regulatedChildCount;
TemplateElement[] newRegulatedChildBuffer = new TemplateElement[capacity];
for (int i = 0; i < ln; i++) {
newRegulatedChildBuffer[i] = regulatedChildBuffer[i];
}
regulatedChildBuffer = newRegulatedChildBuffer;
}
final void addRegulatedChild(TemplateElement nestedElement) {
addRegulatedChild(regulatedChildCount, nestedElement);
}
final void addRegulatedChild(int index, TemplateElement nestedElement) {
final int lRegulatedChildCount = regulatedChildCount;
TemplateElement[] lRegulatedChildBuffer = regulatedChildBuffer;
if (lRegulatedChildBuffer == null) {
lRegulatedChildBuffer = new TemplateElement[INITIAL_REGULATED_CHILD_BUFFER_CAPACITY];
regulatedChildBuffer = lRegulatedChildBuffer;
} else if (lRegulatedChildCount == lRegulatedChildBuffer.length) {
setRegulatedChildBufferCapacity(lRegulatedChildCount != 0 ? lRegulatedChildCount * 2 : 1);
lRegulatedChildBuffer = regulatedChildBuffer;
}
// At this point: nestedElements == this.nestedElements, and has sufficient capacity.
for (int i = lRegulatedChildCount; i > index; i--) {
TemplateElement movedElement = lRegulatedChildBuffer[i - 1];
movedElement.index = i;
lRegulatedChildBuffer[i] = movedElement;
}
nestedElement.index = index;
nestedElement.parent = this;
lRegulatedChildBuffer[index] = nestedElement;
regulatedChildCount = lRegulatedChildCount + 1;
}
final int getRegulatedChildCount() {
return regulatedChildCount;
}
final TemplateElement getRegulatedChild(int index) {
return regulatedChildBuffer[index];
}
final int getIndex() {
return index;
}
/**
* The element whose child this element is, or {@code null} if this is the root node.
*/
final TemplateElement getParentElement() {
return parent;
}
final TemplateElement getNestedBlock() {
return nestedBlock;
}
final void setNestedBlock(TemplateElement nestedBlock) {
if (nestedBlock != null) {
nestedBlock.parent = this;
nestedBlock.index = 0;
}
this.nestedBlock = nestedBlock;
}
/**
* This is a special case, because a root element is not contained in another element, so we couldn't set the
* private fields.
*/
final void setFieldsForRootElement() {
index = 0;
parent = null;
}
/**
* Walk the AST subtree rooted by this element, and do simplifications where possible, also remove superfluous
* whitespace.
*
* @param stripWhitespace
* whether to remove superfluous whitespace
*
* @return The element this element should be replaced with in the parent. If it's the same as this element, no
* actual replacement will happen. Note that adjusting the {@link #parent} and {@link #index} of the result
* is the duty of the caller, not of this method.
*/
TemplateElement postParseCleanup(boolean stripWhitespace) throws ParseException {
int regulatedChildCount = this.regulatedChildCount;
if (regulatedChildCount != 0) {
for (int i = 0; i < regulatedChildCount; i++) {
TemplateElement te = regulatedChildBuffer[i];
te = te.postParseCleanup(stripWhitespace);
regulatedChildBuffer[i] = te;
te.parent = this;
te.index = i;
}
if (stripWhitespace) {
for (int i = 0; i < regulatedChildCount; i++) {
TemplateElement te = regulatedChildBuffer[i];
if (te.isIgnorable()) {
regulatedChildCount--;
for (int j = i; j < regulatedChildCount; j++) {
final TemplateElement te2 = regulatedChildBuffer[j + 1];
regulatedChildBuffer[j] = te2;
te2.index = j;
}
regulatedChildBuffer[regulatedChildCount] = null;
this.regulatedChildCount = regulatedChildCount;
i--;
}
}
}
if (regulatedChildCount < regulatedChildBuffer.length
&& regulatedChildCount <= regulatedChildBuffer.length * 3 / 4) {
TemplateElement[] trimmedregulatedChildBuffer = new TemplateElement[regulatedChildCount];
for (int i = 0; i < regulatedChildCount; i++) {
trimmedregulatedChildBuffer[i] = regulatedChildBuffer[i];
}
regulatedChildBuffer = trimmedregulatedChildBuffer;
}
} else if (nestedBlock != null) {
nestedBlock = nestedBlock.postParseCleanup(stripWhitespace);
if (nestedBlock.isIgnorable()) {
nestedBlock = null;
} else {
nestedBlock.parent = this;
}
}
return this;
}
boolean isIgnorable() {
return false;
}
// The following methods exist to support some fancier tree-walking
// and were introduced to support the whitespace cleanup feature in 2.2
TemplateElement prevTerminalNode() {
TemplateElement prev = previousSibling();
if (prev != null) {
return prev.getLastLeaf();
} else if (parent != null) {
return parent.prevTerminalNode();
}
return null;
}
TemplateElement nextTerminalNode() {
TemplateElement next = nextSibling();
if (next != null) {
return next.getFirstLeaf();
} else if (parent != null) {
return parent.nextTerminalNode();
}
return null;
}
TemplateElement previousSibling() {
if (parent == null) {
return null;
}
return index > 0 ? parent.regulatedChildBuffer[index - 1] : null;
}
TemplateElement nextSibling() {
if (parent == null) {
return null;
}
return index + 1 < parent.regulatedChildCount ? parent.regulatedChildBuffer[index + 1] : null;
}
private TemplateElement getFirstChild() {
if (nestedBlock != null) {
return nestedBlock;
}
if (regulatedChildCount == 0) {
return null;
}
return regulatedChildBuffer[0];
}
private TemplateElement getLastChild() {
if (nestedBlock != null) {
return nestedBlock;
}
final int regulatedChildCount = this.regulatedChildCount;
if (regulatedChildCount == 0) {
return null;
}
return regulatedChildBuffer[regulatedChildCount - 1];
}
private TemplateElement getFirstLeaf() {
TemplateElement te = this;
while (!te.isLeaf() && !(te instanceof Macro) && !(te instanceof BlockAssignment)) {
// A macro or macro invocation is treated as a leaf here for special reasons
te = te.getFirstChild();
}
return te;
}
private TemplateElement getLastLeaf() {
TemplateElement te = this;
while (!te.isLeaf() && !(te instanceof Macro) && !(te instanceof BlockAssignment)) {
// A macro or macro invocation is treated as a leaf here for special reasons
te = te.getLastChild();
}
return te;
}
/**
* Tells if executing this element has output that only depends on the template content and that has no side
* effects.
*/
boolean isOutputCacheable() {
return false;
}
/**
* determines whether this element's presence on a line
* indicates that we should not strip opening whitespace
* in the post-parse whitespace gobbling step.
*/
boolean heedsOpeningWhitespace() {
return false;
}
/**
* determines whether this element's presence on a line
* indicates that we should not strip trailing whitespace
* in the post-parse whitespace gobbling step.
*/
boolean heedsTrailingWhitespace() {
return false;
}
}