http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java new file mode 100644 index 0000000..753202b --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.impl.SimpleNumber; + +/** + * AST expression node: {@code -exp} or {@code +exp}. + */ +final class ASTExpNegateOrPlus extends ASTExpression { + + private final int TYPE_MINUS = 0; + private final int TYPE_PLUS = 1; + + private final ASTExpression target; + private final boolean isMinus; + private static final Integer MINUS_ONE = Integer.valueOf(-1); + + ASTExpNegateOrPlus(ASTExpression target, boolean isMinus) { + this.target = target; + this.isMinus = isMinus; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateNumberModel targetModel = null; + TemplateModel tm = target.eval(env); + try { + targetModel = (TemplateNumberModel) tm; + } catch (ClassCastException cce) { + throw new NonNumericalException(target, tm, env); + } + if (!isMinus) { + return targetModel; + } + target.assertNonNull(targetModel, env); + Number n = targetModel.getAsNumber(); + n = ArithmeticEngine.CONSERVATIVE_ENGINE.multiply(MINUS_ONE, n); + return new SimpleNumber(n); + } + + @Override + public String getCanonicalForm() { + String op = isMinus ? "-" : "+"; + return op + target.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return isMinus ? "-..." : "+..."; + } + + @Override + boolean isLiteral() { + return target.isLiteral(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpNegateOrPlus( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + isMinus); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return target; + case 1: return Integer.valueOf(isMinus ? TYPE_MINUS : TYPE_PLUS); + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.RIGHT_HAND_OPERAND; + case 1: return ParameterRole.AST_NODE_SUBTYPE; + default: throw new IndexOutOfBoundsException(); + } + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpNot.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNot.java b/src/main/java/org/apache/freemarker/core/ASTExpNot.java new file mode 100644 index 0000000..5aed612 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpNot.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +/** + * AST expression node: {@code !exp}. + */ +final class ASTExpNot extends ASTExpBoolean { + + private final ASTExpression target; + + ASTExpNot(ASTExpression target) { + this.target = target; + } + + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return (!target.evalToBoolean(env)); + } + + @Override + public String getCanonicalForm() { + return "!" + target.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return "!"; + } + + @Override + boolean isLiteral() { + return target.isLiteral(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpNot( + target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return target; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.RIGHT_HAND_OPERAND; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java b/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java new file mode 100644 index 0000000..c60c34e --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpNumberLiteral.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.impl.SimpleNumber; + +/** + * AST expression node: numerical literal + */ +final class ASTExpNumberLiteral extends ASTExpression implements TemplateNumberModel { + + private final Number value; + + public ASTExpNumberLiteral(Number value) { + this.value = value; + } + + @Override + TemplateModel _eval(Environment env) { + return new SimpleNumber(value); + } + + @Override + public String evalAndCoerceToPlainText(Environment env) throws TemplateException { + return env.formatNumberToPlainText(this, this, false); + } + + @Override + public Number getAsNumber() { + return value; + } + + String getName() { + return "the number: '" + value + "'"; + } + + @Override + public String getCanonicalForm() { + return value.toString(); + } + + @Override + String getNodeTypeSymbol() { + return getCanonicalForm(); + } + + @Override + boolean isLiteral() { + return true; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpNumberLiteral(value); + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpOr.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpOr.java b/src/main/java/org/apache/freemarker/core/ASTExpOr.java new file mode 100644 index 0000000..e01e875 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpOr.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +/** + * AST expression node: {@code exp || exp}. + */ +final class ASTExpOr extends ASTExpBoolean { + + private final ASTExpression lho; + private final ASTExpression rho; + + ASTExpOr(ASTExpression lho, ASTExpression rho) { + this.lho = lho; + this.rho = rho; + } + + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return lho.evalToBoolean(env) || rho.evalToBoolean(env); + } + + @Override + public String getCanonicalForm() { + return lho.getCanonicalForm() + " || " + rho.getCanonicalForm(); + } + + @Override + String getNodeTypeSymbol() { + return "||"; + } + + @Override + boolean isLiteral() { + return constantValue != null || (lho.isLiteral() && rho.isLiteral()); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpOr( + lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return lho; + case 1: return rho; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.forBinaryOperatorOperand(idx); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java b/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java new file mode 100644 index 0000000..9803498 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpParenthesis.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateModel; + +/** + * AST expression node: {@code (exp)}. + */ +final class ASTExpParenthesis extends ASTExpression { + + private final ASTExpression nested; + + ASTExpParenthesis(ASTExpression nested) { + this.nested = nested; + } + + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + return nested.evalToBoolean(env); + } + + @Override + public String getCanonicalForm() { + return "(" + nested.getCanonicalForm() + ")"; + } + + @Override + String getNodeTypeSymbol() { + return "(...)"; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return nested.eval(env); + } + + @Override + public boolean isLiteral() { + return nested.isLiteral(); + } + + ASTExpression getNestedExpression() { + return nested; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpParenthesis( + nested.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return nested; + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.ENCLOSED_OPERAND; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpRange.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpRange.java b/src/main/java/org/apache/freemarker/core/ASTExpRange.java new file mode 100644 index 0000000..0910517 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpRange.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.BugException; + +/** + * AST expression node: {@code exp .. exp}, {@code exp ..< exp} (or {@code exp ..! exp}), {@code exp ..* exp}. + */ +final class ASTExpRange extends ASTExpression { + + static final int END_INCLUSIVE = 0; + static final int END_EXCLUSIVE = 1; + static final int END_UNBOUND = 2; + static final int END_SIZE_LIMITED = 3; + + final ASTExpression lho; + final ASTExpression rho; + final int endType; + + ASTExpRange(ASTExpression lho, ASTExpression rho, int endType) { + this.lho = lho; + this.rho = rho; + this.endType = endType; + } + + int getEndType() { + return endType; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + final int begin = lho.evalToNumber(env).intValue(); + if (endType != END_UNBOUND) { + final int lhoValue = rho.evalToNumber(env).intValue(); + return new BoundedRangeModel( + begin, endType != END_SIZE_LIMITED ? lhoValue : begin + lhoValue, + endType == END_INCLUSIVE, endType == END_SIZE_LIMITED); + } else { + return new ListableRightUnboundedRangeModel(begin); + } + } + + // Surely this way we can tell that it won't be a boolean without evaluating the range, but why was this important? + @Override + boolean evalToBoolean(Environment env) throws TemplateException { + throw new NonBooleanException(this, new BoundedRangeModel(0, 0, false, false), env); + } + + @Override + public String getCanonicalForm() { + String rhs = rho != null ? rho.getCanonicalForm() : ""; + return lho.getCanonicalForm() + getNodeTypeSymbol() + rhs; + } + + @Override + String getNodeTypeSymbol() { + switch (endType) { + case END_EXCLUSIVE: return "..<"; + case END_INCLUSIVE: return ".."; + case END_UNBOUND: return ".."; + case END_SIZE_LIMITED: return "..*"; + default: throw new BugException(endType); + } + } + + @Override + boolean isLiteral() { + boolean rightIsLiteral = rho == null || rho.isLiteral(); + return constantValue != null || (lho.isLiteral() && rightIsLiteral); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + return new ASTExpRange( + lho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + rho.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), + endType); + } + + @Override + int getParameterCount() { + return 2; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return lho; + case 1: return rho; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + return ParameterRole.forBinaryOperatorOperand(idx); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java b/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java new file mode 100644 index 0000000..b7c8960 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpStringLiteral.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import java.io.StringReader; +import java.util.List; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.util.FTLUtil; + +/** + * AST expression node: string literal + */ +final class ASTExpStringLiteral extends ASTExpression implements TemplateScalarModel { + + private final String value; + + /** {@link List} of {@link String}-s and {@link ASTInterpolation}-s. */ + private List<Object> dynamicValue; + + ASTExpStringLiteral(String value) { + this.value = value; + } + + /** + * @param parentTkMan + * The token source of the template that contains this string literal. As of this writing, we only need + * this to share the {@code namingConvetion} with that. + */ + void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException { + // The way this works is incorrect (the literal should be parsed without un-escaping), + // but we can't fix this backward compatibly. + if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) { + + Template parentTemplate = getTemplate(); + ParserConfiguration pcfg = parentTemplate.getParserConfiguration(); + + try { + SimpleCharStream simpleCharacterStream = new SimpleCharStream( + new StringReader(value), + beginLine, beginColumn + 1, + value.length()); + simpleCharacterStream.setTabSize(pcfg.getTabSize()); + + FMParserTokenManager tkMan = new FMParserTokenManager( + simpleCharacterStream); + + FMParser parser = new FMParser(parentTemplate, false, tkMan, pcfg, + TemplateSpecifiedEncodingHandler.DEFAULT); + // We continue from the parent parser's current state: + parser.setupStringLiteralMode(parentTkMan, outputFormat); + try { + dynamicValue = parser.StaticTextAndInterpolations(); + } finally { + // The parent parser continues from this parser's current state: + parser.tearDownStringLiteralMode(parentTkMan); + } + } catch (ParseException e) { + e.setTemplateName(parentTemplate.getSourceName()); + throw e; + } + constantValue = null; + } + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + if (dynamicValue == null) { + return new SimpleScalar(value); + } else { + // This should behave like concatenating the values with `+`. Thus, an interpolated expression that + // returns markup promotes the result of the whole expression to markup. + + // Exactly one of these is non-null, depending on if the result will be plain text or markup, which can + // change during evaluation, depending on the result of the interpolations: + StringBuilder plainTextResult = null; + TemplateMarkupOutputModel<?> markupResult = null; + + for (Object part : dynamicValue) { + Object calcedPart = + part instanceof String ? part + : ((ASTInterpolation) part).calculateInterpolatedStringOrMarkup(env); + if (markupResult != null) { + TemplateMarkupOutputModel<?> partMO = calcedPart instanceof String + ? markupResult.getOutputFormat().fromPlainTextByEscaping((String) calcedPart) + : (TemplateMarkupOutputModel<?>) calcedPart; + markupResult = EvalUtil.concatMarkupOutputs(this, markupResult, partMO); + } else { // We are using `plainTextOutput` (or nothing yet) + if (calcedPart instanceof String) { + String partStr = (String) calcedPart; + if (plainTextResult == null) { + plainTextResult = new StringBuilder(partStr); + } else { + plainTextResult.append(partStr); + } + } else { // `calcedPart` is TemplateMarkupOutputModel + TemplateMarkupOutputModel<?> moPart = (TemplateMarkupOutputModel<?>) calcedPart; + if (plainTextResult != null) { + TemplateMarkupOutputModel<?> leftHandMO = moPart.getOutputFormat() + .fromPlainTextByEscaping(plainTextResult.toString()); + markupResult = EvalUtil.concatMarkupOutputs(this, leftHandMO, moPart); + plainTextResult = null; + } else { + markupResult = moPart; + } + } + } + } // for each part + return markupResult != null ? markupResult + : plainTextResult != null ? new SimpleScalar(plainTextResult.toString()) + : SimpleScalar.EMPTY_STRING; + } + } + + @Override + public String getAsString() { + return value; + } + + /** + * Tells if this is something like <tt>"${foo}"</tt>, which is usually a user mistake. + */ + boolean isSingleInterpolationLiteral() { + return dynamicValue != null && dynamicValue.size() == 1 + && dynamicValue.get(0) instanceof ASTInterpolation; + } + + @Override + public String getCanonicalForm() { + if (dynamicValue == null) { + return FTLUtil.toStringLiteral(value); + } else { + StringBuilder sb = new StringBuilder(); + sb.append('"'); + for (Object child : dynamicValue) { + if (child instanceof ASTInterpolation) { + sb.append(((ASTInterpolation) child).getCanonicalFormInStringLiteral()); + } else { + sb.append(FTLUtil.escapeStringLiteralPart((String) child, '"')); + } + } + sb.append('"'); + return sb.toString(); + } + } + + @Override + String getNodeTypeSymbol() { + return dynamicValue == null ? getCanonicalForm() : "dynamic \"...\""; + } + + @Override + boolean isLiteral() { + return dynamicValue == null; + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ASTExpStringLiteral cloned = new ASTExpStringLiteral(value); + // FIXME: replacedIdentifier should be searched inside interpolatedOutput too: + cloned.dynamicValue = dynamicValue; + return cloned; + } + + @Override + int getParameterCount() { + return dynamicValue == null ? 0 : dynamicValue.size(); + } + + @Override + Object getParameterValue(int idx) { + checkIndex(idx); + return dynamicValue.get(idx); + } + + private void checkIndex(int idx) { + if (dynamicValue == null || idx >= dynamicValue.size()) { + throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + checkIndex(idx); + return ParameterRole.VALUE_PART; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpVariable.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpVariable.java b/src/main/java/org/apache/freemarker/core/ASTExpVariable.java new file mode 100644 index 0000000..b3cfbc2 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpVariable.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST expression node: Reference to a "top-level" (local, current namespace, global, data-model) variable + */ +final class ASTExpVariable extends ASTExpression { + + private final String name; + + ASTExpVariable(String name) { + this.name = name; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + try { + return env.getVariable(name); + } catch (NullPointerException e) { + if (env == null) { + throw new _MiscTemplateException( + "Variables are not available (certainly you are in a parse-time executed directive). " + + "The name of the variable you tried to read: ", name); + } else { + throw e; + } + } + } + + @Override + public String getCanonicalForm() { + return _StringUtil.toFTLTopLevelIdentifierReference(name); + } + + /** + * The name of the identifier without any escaping or other syntactical distortions. + */ + String getName() { + return name; + } + + @Override + String getNodeTypeSymbol() { + return getCanonicalForm(); + } + + @Override + boolean isLiteral() { + return false; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + protected ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + if (name.equals(replacedIdentifier)) { + if (replacementState.replacementAlreadyInUse) { + ASTExpression clone = replacement.deepCloneWithIdentifierReplaced(null, null, replacementState); + clone.copyLocationFrom(replacement); + return clone; + } else { + replacementState.replacementAlreadyInUse = true; + return replacement; + } + } else { + return new ASTExpVariable(name); + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTExpression.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpression.java b/src/main/java/org/apache/freemarker/core/ASTExpression.java new file mode 100644 index 0000000..692f1bb --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTExpression.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.beans.BeanModel; + +/** + * AST expression node superclass + */ +abstract class ASTExpression extends ASTNode { + + /** + * @param env might be {@code null}, if this kind of expression can be evaluated during parsing (as opposed to + * during template execution). + */ + abstract TemplateModel _eval(Environment env) throws TemplateException; + + abstract boolean isLiteral(); + + // Used to store a constant return value for this expression. Only if it + // is possible, of course. + + TemplateModel constantValue; + + // Hook in here to set the constant value if possible. + + @Override + void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) { + super.setLocation(template, beginColumn, beginLine, endColumn, endLine); + if (isLiteral()) { + try { + constantValue = _eval(null); + } catch (Exception e) { + // deliberately ignore. + } + } + } + + final TemplateModel getAsTemplateModel(Environment env) throws TemplateException { + return eval(env); + } + + final TemplateModel eval(Environment env) throws TemplateException { + return constantValue != null ? constantValue : _eval(env); + } + + String evalAndCoerceToPlainText(Environment env) throws TemplateException { + return EvalUtil.coerceModelToPlainText(eval(env), this, null, env); + } + + /** + * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection. + */ + String evalAndCoerceToPlainText(Environment env, String seqTip) throws TemplateException { + return EvalUtil.coerceModelToPlainText(eval(env), this, seqTip, env); + } + + Object evalAndCoerceToStringOrMarkup(Environment env) throws TemplateException { + return EvalUtil.coerceModelToStringOrMarkup(eval(env), this, null, env); + } + + /** + * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection. + */ + Object evalAndCoerceToStringOrMarkup(Environment env, String seqTip) throws TemplateException { + return EvalUtil.coerceModelToStringOrMarkup(eval(env), this, seqTip, env); + } + + String evalAndCoerceToStringOrUnsupportedMarkup(Environment env) throws TemplateException { + return EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, null, env); + } + + /** + * @param seqTip Tip to display if the value type is not coercable, but it's sequence or collection. + */ + String evalAndCoerceToStringOrUnsupportedMarkup(Environment env, String seqTip) throws TemplateException { + return EvalUtil.coerceModelToStringOrUnsupportedMarkup(eval(env), this, seqTip, env); + } + + Number evalToNumber(Environment env) throws TemplateException { + TemplateModel model = eval(env); + return modelToNumber(model, env); + } + + Number modelToNumber(TemplateModel model, Environment env) throws TemplateException { + if (model instanceof TemplateNumberModel) { + return EvalUtil.modelToNumber((TemplateNumberModel) model, this); + } else { + throw new NonNumericalException(this, model, env); + } + } + + boolean evalToBoolean(Environment env) throws TemplateException { + return evalToBoolean(env, null); + } + + boolean evalToBoolean(Configuration cfg) throws TemplateException { + return evalToBoolean(null, cfg); + } + + TemplateModel evalToNonMissing(Environment env) throws TemplateException { + TemplateModel result = eval(env); + assertNonNull(result, env); + return result; + } + + private boolean evalToBoolean(Environment env, Configuration cfg) throws TemplateException { + TemplateModel model = eval(env); + return modelToBoolean(model, env, cfg); + } + + boolean modelToBoolean(TemplateModel model, Environment env) throws TemplateException { + return modelToBoolean(model, env, null); + } + + boolean modelToBoolean(TemplateModel model, Configuration cfg) throws TemplateException { + return modelToBoolean(model, null, cfg); + } + + private boolean modelToBoolean(TemplateModel model, Environment env, Configuration cfg) throws TemplateException { + if (model instanceof TemplateBooleanModel) { + return ((TemplateBooleanModel) model).getAsBoolean(); + } else { + throw new NonBooleanException(this, model, env); + } + } + + final ASTExpression deepCloneWithIdentifierReplaced( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) { + ASTExpression clone = deepCloneWithIdentifierReplaced_inner(replacedIdentifier, replacement, replacementState); + if (clone.beginLine == 0) { + clone.copyLocationFrom(this); + } + return clone; + } + + static class ReplacemenetState { + /** + * If the replacement expression is not in use yet, we don't have to clone it. + */ + boolean replacementAlreadyInUse; + } + + /** + * This should return an equivalent new expression object (or an identifier replacement expression). + * The position need not be filled, unless it will be different from the position of what we were cloning. + */ + protected abstract ASTExpression deepCloneWithIdentifierReplaced_inner( + String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState); + + static boolean isEmpty(TemplateModel model) throws TemplateModelException { + if (model instanceof BeanModel) { + return ((BeanModel) model).isEmpty(); + } else if (model instanceof TemplateSequenceModel) { + return ((TemplateSequenceModel) model).size() == 0; + } else if (model instanceof TemplateScalarModel) { + String s = ((TemplateScalarModel) model).getAsString(); + return (s == null || s.length() == 0); + } else if (model == null) { + return true; + } else if (model instanceof TemplateMarkupOutputModel) { // Note: happens just after FTL string check + TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) model; + return mo.getOutputFormat().isEmpty(mo); + } else if (model instanceof TemplateCollectionModel) { + return !((TemplateCollectionModel) model).iterator().hasNext(); + } else if (model instanceof TemplateHashModel) { + return ((TemplateHashModel) model).isEmpty(); + } else if (model instanceof TemplateNumberModel + || model instanceof TemplateDateModel + || model instanceof TemplateBooleanModel) { + return false; + } else { + return true; + } + } + + void assertNonNull(TemplateModel model, Environment env) throws InvalidReferenceException { + if (model == null) throw InvalidReferenceException.getInstance(this, env); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java b/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java new file mode 100644 index 0000000..93a95c5 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTHashInterpolation.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import java.io.IOException; +import java.io.Writer; +import java.text.NumberFormat; +import java.util.Locale; + +import org.apache.freemarker.core.util.FTLUtil; + +/** + * AST interpolation node: <tt>#{exp}</tt> + */ +final class ASTHashInterpolation extends ASTInterpolation { + + private final ASTExpression expression; + private final boolean hasFormat; + private final int minFracDigits; + private final int maxFracDigits; + /** For OutputFormat-based auto-escaping */ + private final MarkupOutputFormat autoEscapeOutputFormat; + private volatile FormatHolder formatCache; // creating new NumberFormat is slow operation + + ASTHashInterpolation(ASTExpression expression, MarkupOutputFormat autoEscapeOutputFormat) { + this.expression = expression; + hasFormat = false; + minFracDigits = 0; + maxFracDigits = 0; + this.autoEscapeOutputFormat = autoEscapeOutputFormat; + } + + ASTHashInterpolation(ASTExpression expression, + int minFracDigits, int maxFracDigits, + MarkupOutputFormat autoEscapeOutputFormat) { + this.expression = expression; + hasFormat = true; + this.minFracDigits = minFracDigits; + this.maxFracDigits = maxFracDigits; + this.autoEscapeOutputFormat = autoEscapeOutputFormat; + } + + @Override + _ASTElement[] accept(Environment env) throws TemplateException, IOException { + String s = calculateInterpolatedStringOrMarkup(env); + Writer out = env.getOut(); + if (autoEscapeOutputFormat != null) { + autoEscapeOutputFormat.output(s, out); + } else { + out.write(s); + } + return null; + } + + @Override + protected String calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException { + Number num = expression.evalToNumber(env); + + FormatHolder fmth = formatCache; // atomic sampling + if (fmth == null || !fmth.locale.equals(env.getLocale())) { + synchronized (this) { + fmth = formatCache; + if (fmth == null || !fmth.locale.equals(env.getLocale())) { + NumberFormat fmt = NumberFormat.getNumberInstance(env.getLocale()); + if (hasFormat) { + fmt.setMinimumFractionDigits(minFracDigits); + fmt.setMaximumFractionDigits(maxFracDigits); + } else { + fmt.setMinimumFractionDigits(0); + fmt.setMaximumFractionDigits(50); + } + fmt.setGroupingUsed(false); + formatCache = new FormatHolder(fmt, env.getLocale()); + fmth = formatCache; + } + } + } + // We must use Format even if hasFormat == false. + // Some locales may use non-Arabic digits, thus replacing the + // decimal separator in the result of toString() is not enough. + String s = fmth.format.format(num); + return s; + } + + @Override + protected String dump(boolean canonical, boolean inStringLiteral) { + StringBuilder buf = new StringBuilder("#{"); + final String exprCF = expression.getCanonicalForm(); + buf.append(inStringLiteral ? FTLUtil.escapeStringLiteralPart(exprCF, '"') : exprCF); + if (hasFormat) { + buf.append(" ; "); + buf.append("m"); + buf.append(minFracDigits); + buf.append("M"); + buf.append(maxFracDigits); + } + buf.append("}"); + return buf.toString(); + } + + @Override + String getNodeTypeSymbol() { + return "#{...}"; + } + + @Override + boolean heedsOpeningWhitespace() { + return true; + } + + @Override + boolean heedsTrailingWhitespace() { + return true; + } + + private static class FormatHolder { + final NumberFormat format; + final Locale locale; + + FormatHolder(NumberFormat format, Locale locale) { + this.format = format; + this.locale = locale; + } + } + + @Override + int getParameterCount() { + return 3; + } + + @Override + Object getParameterValue(int idx) { + switch (idx) { + case 0: return expression; + case 1: return Integer.valueOf(minFracDigits); + case 2: return Integer.valueOf(maxFracDigits); + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + ParameterRole getParameterRole(int idx) { + switch (idx) { + case 0: return ParameterRole.CONTENT; + case 1: return ParameterRole.MINIMUM_DECIMALS; + case 2: return ParameterRole.MAXIMUM_DECIMALS; + default: throw new IndexOutOfBoundsException(); + } + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java b/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java new file mode 100644 index 0000000..63b0f77 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTImplicitParent.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import java.io.IOException; + +/** + * AST directive-like node, used where there's no other parent for a list of {@link _ASTElement}-s. Most often occurs as + * the root node of the AST. + */ +final class ASTImplicitParent extends _ASTElement { + + ASTImplicitParent() { } + + @Override + _ASTElement postParseCleanup(boolean stripWhitespace) + throws ParseException { + super.postParseCleanup(stripWhitespace); + return getChildCount() == 1 ? getChild(0) : this; + } + + /** + * Processes the contents of the internal <tt>_ASTElement</tt> list, + * and outputs the resulting text. + */ + @Override + _ASTElement[] accept(Environment env) + throws TemplateException, IOException { + return getChildBuffer(); + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + return getChildrenCanonicalForm(); + } else { + if (getParentElement() == null) { + return "root"; + } + return getNodeTypeSymbol(); // ASTImplicitParent is uninteresting in a stack trace. + } + } + + @Override + protected boolean isOutputCacheable() { + int ln = getChildCount(); + for (int i = 0; i < ln; i++) { + if (!getChild(i).isOutputCacheable()) { + return false; + } + } + return true; + } + + @Override + String getNodeTypeSymbol() { + return "#mixed_content"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isIgnorable(boolean stripWhitespace) { + return getChildCount() == 0; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTInterpolation.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTInterpolation.java b/src/main/java/org/apache/freemarker/core/ASTInterpolation.java new file mode 100644 index 0000000..d303642 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTInterpolation.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +/** + * AST interpolation node superclass. + */ +abstract class ASTInterpolation extends _ASTElement { + + protected abstract String dump(boolean canonical, boolean inStringLiteral); + + @Override + protected final String dump(boolean canonical) { + return dump(canonical, false); + } + + final String getCanonicalFormInStringLiteral() { + return dump(true, true); + } + + /** + * Returns the already type-converted value that this interpolation will insert into the output. + * + * @return A {@link String} or {@link TemplateMarkupOutputModel}. Not {@code null}. + */ + protected abstract Object calculateInterpolatedStringOrMarkup(Environment env) throws TemplateException; + + @Override + boolean isShownInStackTrace() { + return true; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTNode.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTNode.java b/src/main/java/org/apache/freemarker/core/ASTNode.java new file mode 100644 index 0000000..19dd62d --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTNode.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +/** + * AST node: The superclass of all AST nodes + */ +abstract class ASTNode { + + private Template template; + int beginColumn, beginLine, endColumn, endLine; + + /** This is needed for an ?eval hack; the expression AST nodes will be the descendants of the template, however, + * we can't give their position in the template, only in the dynamic string that's evaluated. That's signaled + * by a negative line numbers, starting from this constant as line 1. */ + static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000; + + final void setLocation(Template template, Token begin, Token end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + final void setLocation(Template template, Token tagBegin, Token tagEnd, TemplateElements children) { + _ASTElement lastChild = children.getLast(); + if (lastChild != null) { + // [<#if exp>children]<#else> + setLocation(template, tagBegin, lastChild); + } else { + // [<#if exp>]<#else> + setLocation(template, tagBegin, tagEnd); + } + } + + final void setLocation(Template template, Token begin, ASTNode end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + final void setLocation(Template template, ASTNode begin, Token end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + final void setLocation(Template template, ASTNode begin, ASTNode end) { + setLocation(template, begin.beginColumn, begin.beginLine, end.endColumn, end.endLine); + } + + void setLocation(Template template, int beginColumn, int beginLine, int endColumn, int endLine) { + this.template = template; + this.beginColumn = beginColumn; + this.beginLine = beginLine; + this.endColumn = endColumn; + this.endLine = endLine; + } + + public final int getBeginColumn() { + return beginColumn; + } + + public final int getBeginLine() { + return beginLine; + } + + public final int getEndColumn() { + return endColumn; + } + + public final int getEndLine() { + return endLine; + } + + /** + * Returns a string that indicates + * where in the template source, this object is. + */ + public String getStartLocation() { + return MessageUtil.formatLocationForEvaluationError(template, beginLine, beginColumn); + } + + /** + * As of 2.3.20. the same as {@link #getStartLocation}. Meant to be used where there's a risk of XSS + * when viewing error messages. + */ + public String getStartLocationQuoted() { + return getStartLocation(); + } + + public String getEndLocation() { + return MessageUtil.formatLocationForEvaluationError(template, endLine, endColumn); + } + + /** + * As of 2.3.20. the same as {@link #getEndLocation}. Meant to be used where there's a risk of XSS + * when viewing error messages. + */ + public String getEndLocationQuoted() { + return getEndLocation(); + } + + public final String getSource() { + String s; + if (template != null) { + s = template.getSource(beginColumn, beginLine, endColumn, endLine); + } else { + s = null; + } + + // Can't just return null for backward-compatibility... + return s != null ? s : getCanonicalForm(); + } + + @Override + public String toString() { + String s; + try { + s = getSource(); + } catch (Exception e) { // REVISIT: A bit of a hack? (JR) + s = null; + } + return s != null ? s : getCanonicalForm(); + } + + /** + * @return whether the point in the template file specified by the + * column and line numbers is contained within this template object. + */ + public boolean contains(int column, int line) { + if (line < beginLine || line > endLine) { + return false; + } + if (line == beginLine) { + if (column < beginColumn) { + return false; + } + } + if (line == endLine) { + if (column > endColumn) { + return false; + } + } + return true; + } + + public Template getTemplate() { + return template; + } + + ASTNode copyLocationFrom(ASTNode from) { + template = from.template; + beginColumn = from.beginColumn; + beginLine = from.beginLine; + endColumn = from.endColumn; + endLine = from.endLine; + return this; + } + + /** + * FTL generated from the AST of the node, which must be parseable to an AST that does the same as the original + * source, assuming we turn off automatic white-space removal when parsing the canonical form. + * + * @see _ASTElement#getDescription() + * @see #getNodeTypeSymbol() + */ + abstract public String getCanonicalForm(); + + /** + * A very sort single-line string that describes what kind of AST node this is, without describing any + * embedded expression or child element. Examples: {@code "#if"}, {@code "+"}, <tt>"${...}</tt>. These values should + * be suitable as tree node labels in a tree view. Yet, they should be consistent and complete enough so that an AST + * that is equivalent with the original could be reconstructed from the tree view. Thus, for literal values that are + * leaf nodes the symbols should be the canonical form of value. + * + * Note that {@link _ASTElement#getDescription()} has similar role, only it doesn't go under the element level + * (i.e. down to the expression level), instead it always prints the embedded expressions itself. + * + * @see #getCanonicalForm() + * @see _ASTElement#getDescription() + */ + abstract String getNodeTypeSymbol(); + + /** + * Returns highest valid parameter index + 1. So one should scan indexes with {@link #getParameterValue(int)} + * starting from 0 up until but excluding this. For example, for the binary "+" operator this will give 2, so the + * legal indexes are 0 and 1. Note that if a parameter is optional in a template-object-type and happens to be + * omitted in an instance, this will still return the same value and the value of that parameter will be + * {@code null}. + */ + abstract int getParameterCount(); + + /** + * Returns the value of the parameter identified by the index. For example, the binary "+" operator will have an + * LHO {@link ASTExpression} at index 0, and and RHO {@link ASTExpression} at index 1. Or, the binary "." operator will + * have an LHO {@link ASTExpression} at index 0, and an RHO {@link String}(!) at index 1. Or, the {@code #include} + * directive will have a path {@link ASTExpression} at index 0, a "parse" {@link ASTExpression} at index 1, etc. + * + * <p>The index value doesn't correspond to the source-code location in general. It's an arbitrary identifier + * that corresponds to the role of the parameter instead. This also means that when a parameter is omitted, the + * index of the other parameters won't shift. + * + * @return {@code null} or any kind of {@link Object}, very often an {@link ASTExpression}. However, if there's + * a {@link ASTNode} stored inside the returned value, it must itself be be a {@link ASTNode} + * too, otherwise the AST couldn't be (easily) fully traversed. That is, non-{@link ASTNode} values + * can only be used for leafs. + * + * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. + */ + abstract Object getParameterValue(int idx); + + /** + * Returns the role of the parameter at the given index, like {@link ParameterRole#LEFT_HAND_OPERAND}. + * + * As of this writing (2013-06-17), for directive parameters it will always give {@link ParameterRole#UNKNOWN}, + * because there was no need to be more specific so far. This should be improved as need. + * + * @throws IndexOutOfBoundsException if {@code idx} is less than 0 or not less than {@link #getParameterCount()}. + */ + abstract ParameterRole getParameterRole(int idx); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/ASTStaticText.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTStaticText.java b/src/main/java/org/apache/freemarker/core/ASTStaticText.java new file mode 100644 index 0000000..2d68e30 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/ASTStaticText.java @@ -0,0 +1,408 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import java.io.IOException; + +import org.apache.freemarker.core.util._CollectionUtil; +import org.apache.freemarker.core.util._StringUtil; + +/** + * AST node representing static text. + */ +final class ASTStaticText extends _ASTElement { + + // We're using char[] instead of String for storing the text block because + // Writer.write(String) involves copying the String contents to a char[] + // using String.getChars(), and then calling Writer.write(char[]). By + // using Writer.write(char[]) directly, we avoid array copying on each + // write. + private char[] text; + private final boolean unparsed; + + public ASTStaticText(String text) { + this(text, false); + } + + public ASTStaticText(String text, boolean unparsed) { + this(text.toCharArray(), unparsed); + } + + ASTStaticText(char[] text, boolean unparsed) { + this.text = text; + this.unparsed = unparsed; + } + + void replaceText(String text) { + this.text = text.toCharArray(); + } + + /** + * Simply outputs the text. + * + * @deprecated This is an internal API; don't call or override it. + */ + @Deprecated + @Override + public _ASTElement[] accept(Environment env) + throws IOException { + env.getOut().write(text); + return null; + } + + @Override + protected String dump(boolean canonical) { + if (canonical) { + String text = new String(this.text); + if (unparsed) { + return "<#noparse>" + text + "</#noparse>"; + } + return text; + } else { + return "text " + _StringUtil.jQuote(new String(text)); + } + } + + @Override + String getNodeTypeSymbol() { + return "#text"; + } + + @Override + int getParameterCount() { + return 1; + } + + @Override + Object getParameterValue(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return new String(text); + } + + @Override + ParameterRole getParameterRole(int idx) { + if (idx != 0) throw new IndexOutOfBoundsException(); + return ParameterRole.CONTENT; + } + + @Override + _ASTElement postParseCleanup(boolean stripWhitespace) { + if (text.length == 0) return this; + int openingCharsToStrip = 0, trailingCharsToStrip = 0; + boolean deliberateLeftTrim = deliberateLeftTrim(); + boolean deliberateRightTrim = deliberateRightTrim(); + if (!stripWhitespace || text.length == 0 ) { + return this; + } + _ASTElement parentElement = getParentElement(); + if (isTopLevelTextIfParentIs(parentElement) && previousSibling() == null) { + return this; + } + if (!deliberateLeftTrim) { + trailingCharsToStrip = trailingCharsToStrip(); + } + if (!deliberateRightTrim) { + openingCharsToStrip = openingCharsToStrip(); + } + if (openingCharsToStrip == 0 && trailingCharsToStrip == 0) { + return this; + } + text = substring(text, openingCharsToStrip, text.length - trailingCharsToStrip); + if (openingCharsToStrip > 0) { + beginLine++; + beginColumn = 1; + } + if (trailingCharsToStrip > 0) { + endColumn = 0; + } + return this; + } + + /** + * Scans forward the nodes on the same line to see whether there is a + * deliberate left trim in effect. Returns true if the left trim was present. + */ + private boolean deliberateLeftTrim() { + boolean result = false; + for (_ASTElement elem = nextTerminalNode(); + elem != null && elem.beginLine == endLine; + elem = elem.nextTerminalNode()) { + if (elem instanceof ASTDirTOrTrOrTl) { + ASTDirTOrTrOrTl ti = (ASTDirTOrTrOrTl) elem; + if (!ti.left && !ti.right) { + result = true; + } + if (ti.left) { + result = true; + int lastNewLineIndex = lastNewLineIndex(); + if (lastNewLineIndex >= 0 || beginColumn == 1) { + char[] firstPart = substring(text, 0, lastNewLineIndex + 1); + char[] lastLine = substring(text, 1 + lastNewLineIndex); + if (_StringUtil.isTrimmableToEmpty(lastLine)) { + text = firstPart; + endColumn = 0; + } else { + int i = 0; + while (Character.isWhitespace(lastLine[i])) { + i++; + } + char[] printablePart = substring(lastLine, i); + text = concat(firstPart, printablePart); + } + } + } + } + } + return result; + } + + /** + * Checks for the presence of a t or rt directive on the + * same line. Returns true if the right trim directive was present. + */ + private boolean deliberateRightTrim() { + boolean result = false; + for (_ASTElement elem = prevTerminalNode(); + elem != null && elem.endLine == beginLine; + elem = elem.prevTerminalNode()) { + if (elem instanceof ASTDirTOrTrOrTl) { + ASTDirTOrTrOrTl ti = (ASTDirTOrTrOrTl) elem; + if (!ti.left && !ti.right) { + result = true; + } + if (ti.right) { + result = true; + int firstLineIndex = firstNewLineIndex() + 1; + if (firstLineIndex == 0) { + return false; + } + if (text.length > firstLineIndex + && text[firstLineIndex - 1] == '\r' + && text[firstLineIndex] == '\n') { + firstLineIndex++; + } + char[] trailingPart = substring(text, firstLineIndex); + char[] openingPart = substring(text, 0, firstLineIndex); + if (_StringUtil.isTrimmableToEmpty(openingPart)) { + text = trailingPart; + beginLine++; + beginColumn = 1; + } else { + int lastNonWS = openingPart.length - 1; + while (Character.isWhitespace(text[lastNonWS])) { + lastNonWS--; + } + char[] printablePart = substring(text, 0, lastNonWS + 1); + if (_StringUtil.isTrimmableToEmpty(trailingPart)) { + // THIS BLOCK IS HEINOUS! THERE MUST BE A BETTER WAY! REVISIT (JR) + boolean trimTrailingPart = true; + for (_ASTElement te = nextTerminalNode(); + te != null && te.beginLine == endLine; + te = te.nextTerminalNode()) { + if (te.heedsOpeningWhitespace()) { + trimTrailingPart = false; + } + if (te instanceof ASTDirTOrTrOrTl && ((ASTDirTOrTrOrTl) te).left) { + trimTrailingPart = true; + break; + } + } + if (trimTrailingPart) trailingPart = _CollectionUtil.EMPTY_CHAR_ARRAY; + } + text = concat(printablePart, trailingPart); + } + } + } + } + return result; + } + + private int firstNewLineIndex() { + char[] text = this.text; + for (int i = 0; i < text.length; i++) { + char c = text[i]; + if (c == '\r' || c == '\n' ) { + return i; + } + } + return -1; + } + + private int lastNewLineIndex() { + char[] text = this.text; + for (int i = text.length - 1; i >= 0; i--) { + char c = text[i]; + if (c == '\r' || c == '\n' ) { + return i; + } + } + return -1; + } + + /** + * figures out how many opening whitespace characters to strip + * in the post-parse cleanup phase. + */ + private int openingCharsToStrip() { + int newlineIndex = firstNewLineIndex(); + if (newlineIndex == -1 && beginColumn != 1) { + return 0; + } + ++newlineIndex; + if (text.length > newlineIndex) { + if (newlineIndex > 0 && text[newlineIndex - 1] == '\r' && text[newlineIndex] == '\n') { + ++newlineIndex; + } + } + if (!_StringUtil.isTrimmableToEmpty(text, 0, newlineIndex)) { + return 0; + } + // We look at the preceding elements on the line to see if we should + // strip the opening newline and any whitespace preceding it. + for (_ASTElement elem = prevTerminalNode(); + elem != null && elem.endLine == beginLine; + elem = elem.prevTerminalNode()) { + if (elem.heedsOpeningWhitespace()) { + return 0; + } + } + return newlineIndex; + } + + /** + * figures out how many trailing whitespace characters to strip + * in the post-parse cleanup phase. + */ + private int trailingCharsToStrip() { + int lastNewlineIndex = lastNewLineIndex(); + if (lastNewlineIndex == -1 && beginColumn != 1) { + return 0; + } + if (!_StringUtil.isTrimmableToEmpty(text, lastNewlineIndex + 1)) { + return 0; + } + // We look at the elements afterward on the same line to see if we should + // strip any whitespace after the last newline + for (_ASTElement elem = nextTerminalNode(); + elem != null && elem.beginLine == endLine; + elem = elem.nextTerminalNode()) { + if (elem.heedsTrailingWhitespace()) { + return 0; + } + } + return text.length - (lastNewlineIndex + 1); + } + + @Override + boolean heedsTrailingWhitespace() { + if (isIgnorable(true)) { + return false; + } + for (char c : text) { + if (c == '\n' || c == '\r') { + return false; + } + if (!Character.isWhitespace(c)) { + return true; + } + } + return true; + } + + @Override + boolean heedsOpeningWhitespace() { + if (isIgnorable(true)) { + return false; + } + for (int i = text.length - 1; i >= 0; i--) { + char c = text[i]; + if (c == '\n' || c == '\r') { + return false; + } + if (!Character.isWhitespace(c)) { + return true; + } + } + return true; + } + + @Override + boolean isIgnorable(boolean stripWhitespace) { + if (text == null || text.length == 0) { + return true; + } + if (stripWhitespace) { + if (!_StringUtil.isTrimmableToEmpty(text)) { + return false; + } + _ASTElement parentElement = getParentElement(); + boolean atTopLevel = isTopLevelTextIfParentIs(parentElement); + _ASTElement prevSibling = previousSibling(); + _ASTElement nextSibling = nextSibling(); + return ((prevSibling == null && atTopLevel) || nonOutputtingType(prevSibling)) + && ((nextSibling == null && atTopLevel) || nonOutputtingType(nextSibling)); + } else { + return false; + } + } + + private boolean isTopLevelTextIfParentIs(_ASTElement parentElement) { + return parentElement == null + || parentElement.getParentElement() == null && parentElement instanceof ASTImplicitParent; + } + + + private boolean nonOutputtingType(_ASTElement element) { + return (element instanceof ASTDirMacro || + element instanceof ASTDirAssignment || + element instanceof ASTDirAssignmentsContainer || + element instanceof ASTDirSetting || + element instanceof ASTDirImport || + element instanceof ASTComment); + } + + private static char[] substring(char[] c, int from, int to) { + char[] c2 = new char[to - from]; + System.arraycopy(c, from, c2, 0, c2.length); + return c2; + } + + private static char[] substring(char[] c, int from) { + return substring(c, from, c.length); + } + + private static char[] concat(char[] c1, char[] c2) { + char[] c = new char[c1.length + c2.length]; + System.arraycopy(c1, 0, c, 0, c1.length); + System.arraycopy(c2, 0, c, c1.length, c2.length); + return c; + } + + @Override + boolean isOutputCacheable() { + return true; + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java b/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java new file mode 100644 index 0000000..705346a --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/AliasTargetTemplateValueFormatException.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +/** + * Can't create a template format that the template format refers to (typically thrown by alias template formats). + * + * @since 2.3.24 + */ +class AliasTargetTemplateValueFormatException extends TemplateValueFormatException { + + public AliasTargetTemplateValueFormatException(String message, Throwable cause) { + super(message, cause); + } + + public AliasTargetTemplateValueFormatException(String message) { + super(message); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java b/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java new file mode 100644 index 0000000..2334680 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/AliasTemplateDateFormatFactory.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.freemarker.core; + +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.util._LocaleUtil; + +/** + * Creates an alias to another format, so that the format can be referred to with a simple name in the template, rather + * than as a concrete pattern or other kind of format string. + * + * @since 2.3.24 + */ +public final class AliasTemplateDateFormatFactory extends TemplateDateFormatFactory { + + private final String defaultTargetFormatString; + private final Map<Locale, String> localizedTargetFormatStrings; + + /** + * @param targetFormatString + * The format string this format will be an alias to. + */ + public AliasTemplateDateFormatFactory(String targetFormatString) { + defaultTargetFormatString = targetFormatString; + localizedTargetFormatStrings = null; + } + + /** + * @param defaultTargetFormatString + * The format string this format will be an alias to if there's no locale-specific format string for the + * requested locale in {@code localizedTargetFormatStrings} + * @param localizedTargetFormatStrings + * Maps {@link Locale}-s to format strings. If the desired locale doesn't occur in the map, a less + * specific locale is tried, repeatedly until only the language part remains. For example, if locale is + * {@code new Locale("en", "US", "Linux")}, then these keys will be attempted untol a match is found, in + * this order: {@code new Locale("en", "US", "Linux")}, {@code new Locale("en", "US")}, + * {@code new Locale("en")}. If there's still no matching key, the value of the + * {@code targetFormatString} will be used. + */ + public AliasTemplateDateFormatFactory( + String defaultTargetFormatString, Map<Locale, String> localizedTargetFormatStrings) { + this.defaultTargetFormatString = defaultTargetFormatString; + this.localizedTargetFormatStrings = localizedTargetFormatStrings; + } + + @Override + public TemplateDateFormat get(String params, int dateType, Locale locale, TimeZone timeZone, boolean zonelessInput, + Environment env) throws TemplateValueFormatException { + TemplateFormatUtil.checkHasNoParameters(params); + try { + String targetFormatString; + if (localizedTargetFormatStrings != null) { + Locale lookupLocale = locale; + targetFormatString = localizedTargetFormatStrings.get(lookupLocale); + while (targetFormatString == null + && (lookupLocale = _LocaleUtil.getLessSpecificLocale(lookupLocale)) != null) { + targetFormatString = localizedTargetFormatStrings.get(lookupLocale); + } + } else { + targetFormatString = null; + } + if (targetFormatString == null) { + targetFormatString = defaultTargetFormatString; + } + return env.getTemplateDateFormat(targetFormatString, dateType, locale, timeZone, zonelessInput); + } catch (TemplateValueFormatException e) { + throw new AliasTargetTemplateValueFormatException("Failed to create format based on target format string, " + + _StringUtil.jQuote(params) + ". Reason given: " + e.getMessage(), e); + } + } + +}
