http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java new file mode 100644 index 0000000..20d2c10 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/InvalidReferenceException.java @@ -0,0 +1,167 @@ +/* + * 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; + +/** + * A subclass of {@link TemplateException} that says that an FTL expression has evaluated to {@code null} or it refers + * to something that doesn't exist. At least in FreeMarker 2.3.x these two cases aren't distinguished. + */ +public class InvalidReferenceException extends TemplateException { + + static final InvalidReferenceException FAST_INSTANCE; + static { + Environment prevEnv = Environment.getCurrentEnvironment(); + try { + Environment.setCurrentEnvironment(null); + FAST_INSTANCE = new InvalidReferenceException( + "Invalid reference. Details are unavilable, as this should have been handled by an FTL construct. " + + "If it wasn't, that's problably a bug in FreeMarker.", + null); + } finally { + Environment.setCurrentEnvironment(prevEnv); + } + } + + private static final Object[] TIP = { + "If the failing expression is known to be legally refer to something that's sometimes null or missing, " + + "either specify a default value like myOptionalVar!myDefault, or use ", + "<#if myOptionalVar??>", "when-present", "<#else>", "when-missing", "</#if>", + ". (These only cover the last step of the expression; to cover the whole expression, " + + "use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??" + }; + + private static final Object[] TIP_MISSING_ASSIGNMENT_TARGET = { + "If the target variable is known to be legally null or missing sometimes, instead of something like ", + "<#assign x += 1>", ", you could write ", "<#if x??>", "<#assign x += 1>", "</#if>", + " or ", "<#assign x = (x!0) + 1>" + }; + + private static final String TIP_NO_DOLLAR = + "Variable references must not start with \"$\", unless the \"$\" is really part of the variable name."; + + private static final String TIP_LAST_STEP_DOT = + "It's the step after the last dot that caused this error, not those before it."; + + private static final String TIP_LAST_STEP_SQUARE_BRACKET = + "It's the final [] step that caused this error, not those before it."; + + private static final String TIP_JSP_TAGLIBS = + "The \"JspTaglibs\" variable isn't a core FreeMarker feature; " + + "it's only available when templates are invoked through org.apache.freemarker.servlet.FreemarkerServlet" + + " (or other custom FreeMarker-JSP integration solution)."; + + /** + * Creates and invalid reference exception that contains no information about what was missing or null. + * As such, try to avoid this constructor. + */ + public InvalidReferenceException(Environment env) { + super("Invalid reference: The expression has evaluated to null or refers to something that doesn't exist.", + env); + } + + /** + * Creates and invalid reference exception that contains no programmatically extractable information about the + * blamed expression. As such, try to avoid this constructor, unless need to raise this expression from outside + * the FreeMarker core. + */ + public InvalidReferenceException(String description, Environment env) { + super(description, env); + } + + /** + * This is the recommended constructor, but it's only used internally, and has no backward compatibility guarantees. + * + * @param expression The expression that evaluates to missing or null. The last step of the expression should be + * the failing one, like in {@code goodStep.failingStep.furtherStep} it should only contain + * {@code goodStep.failingStep}. + */ + InvalidReferenceException(_ErrorDescriptionBuilder description, Environment env, ASTExpression expression) { + super(null, env, expression, description); + } + + /** + * Use this whenever possible, as it returns {@link #FAST_INSTANCE} instead of creating a new instance, when + * appropriate. + */ + static InvalidReferenceException getInstance(ASTExpression blamed, Environment env) { + if (env != null && env.getFastInvalidReferenceExceptions()) { + return FAST_INSTANCE; + } else { + if (blamed != null) { + final _ErrorDescriptionBuilder errDescBuilder + = new _ErrorDescriptionBuilder("The following has evaluated to null or missing:").blame(blamed); + if (endsWithDollarVariable(blamed)) { + errDescBuilder.tips(TIP_NO_DOLLAR, TIP); + } else if (blamed instanceof ASTExpDot) { + final String rho = ((ASTExpDot) blamed).getRHO(); + String nameFixTip = null; + if ("size".equals(rho)) { + nameFixTip = "To query the size of a collection or map use ?size, like myList?size"; + } else if ("length".equals(rho)) { + nameFixTip = "To query the length of a string use ?length, like myString?size"; + } + errDescBuilder.tips( + nameFixTip == null + ? new Object[] { TIP_LAST_STEP_DOT, TIP } + : new Object[] { TIP_LAST_STEP_DOT, nameFixTip, TIP }); + } else if (blamed instanceof ASTExpDynamicKeyName) { + errDescBuilder.tips(TIP_LAST_STEP_SQUARE_BRACKET, TIP); + } else if (blamed instanceof ASTExpVariable + && ((ASTExpVariable) blamed).getName().equals("JspTaglibs")) { + errDescBuilder.tips(TIP_JSP_TAGLIBS, TIP); + } else { + errDescBuilder.tip(TIP); + } + return new InvalidReferenceException(errDescBuilder, env, blamed); + } else { + return new InvalidReferenceException(env); + } + } + } + + /** + * Used for assignments that use operators like {@code +=}, when the target variable was null/missing. + */ + static InvalidReferenceException getInstance(String missingAssignedVarName, String assignmentOperator, + Environment env) { + if (env != null && env.getFastInvalidReferenceExceptions()) { + return FAST_INSTANCE; + } else { + final _ErrorDescriptionBuilder errDescBuilder = new _ErrorDescriptionBuilder( + "The target variable of the assignment, ", + new _DelayedJQuote(missingAssignedVarName), + ", was null or missing, but the \"", + assignmentOperator, "\" operator needs to get its value before assigning to it." + ); + if (missingAssignedVarName.startsWith("$")) { + errDescBuilder.tips(TIP_NO_DOLLAR, TIP_MISSING_ASSIGNMENT_TARGET); + } else { + errDescBuilder.tip(TIP_MISSING_ASSIGNMENT_TARGET); + } + return new InvalidReferenceException(errDescBuilder, env, null); + } + } + + private static boolean endsWithDollarVariable(ASTExpression blame) { + return blame instanceof ASTExpVariable && ((ASTExpVariable) blame).getName().startsWith("$") + || blame instanceof ASTExpDot && ((ASTExpDot) blame).getRHO().startsWith("$"); + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java new file mode 100644 index 0000000..a4a14b5 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ListableRightUnboundedRangeModel.java @@ -0,0 +1,97 @@ +/* + * 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.math.BigInteger; + +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.impl.SimpleNumber; + +/** + * This is the model used for right-unbounded ranges since Incompatible Improvements 2.3.21. + * + * @since 2.3.21 + */ +final class ListableRightUnboundedRangeModel extends RightUnboundedRangeModel implements TemplateCollectionModel { + + ListableRightUnboundedRangeModel(int begin) { + super(begin); + } + + @Override + public int size() throws TemplateModelException { + return Integer.MAX_VALUE; + } + + @Override + public TemplateModelIterator iterator() throws TemplateModelException { + return new TemplateModelIterator() { + boolean needInc; + int nextType = 1; + int nextInt = getBegining(); + long nextLong; + BigInteger nextBigInteger; + + @Override + public TemplateModel next() throws TemplateModelException { + if (needInc) { + switch (nextType) { + case 1: + if (nextInt < Integer.MAX_VALUE) { + nextInt++; + } else { + nextType = 2; + nextLong = nextInt + 1L; + } + break; + + case 2: + if (nextLong < Long.MAX_VALUE) { + nextLong++; + } else { + nextType = 3; + nextBigInteger = BigInteger.valueOf(nextLong); + nextBigInteger = nextBigInteger.add(BigInteger.ONE); + } + break; + + default: // 3 + nextBigInteger = nextBigInteger.add(BigInteger.ONE); + } + } + needInc = true; + return nextType == 1 ? new SimpleNumber(nextInt) + : (nextType == 2 ? new SimpleNumber(nextLong) + : new SimpleNumber(nextBigInteger)); + } + + @Override + public boolean hasNext() throws TemplateModelException { + return true; + } + + }; + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.java new file mode 100644 index 0000000..1084470 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContext.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; + +import java.util.Collection; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; + +/** + * An interface that represents a local context. This is used as the abstraction for + * the context of a ASTDirMacro invocation, a loop, or the nested block call from within + * a macro. + */ +public interface LocalContext { + TemplateModel getLocalVariable(String name) throws TemplateModelException; + Collection getLocalVariableNames() throws TemplateModelException; + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java new file mode 100644 index 0000000..aead89d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/LocalContextStack.java @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * Class that's a little bit more efficient than using an {@code ArrayList<LocalContext>}. + * + * @since 2.3.24 + */ +final class LocalContextStack { + + private LocalContext[] buffer = new LocalContext[8]; + private int size; + + void push(LocalContext localContext) { + final int newSize = ++size; + LocalContext[] buffer = this.buffer; + if (buffer.length < newSize) { + final LocalContext[] newBuffer = new LocalContext[newSize * 2]; + for (int i = 0; i < buffer.length; i++) { + newBuffer[i] = buffer[i]; + } + buffer = newBuffer; + this.buffer = newBuffer; + } + buffer[newSize - 1] = localContext; + } + + void pop() { + buffer[--size] = null; + } + + public LocalContext get(int index) { + return buffer[index]; + } + + public int size() { + return size; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java new file mode 100644 index 0000000..d477963 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MarkupOutputFormatBoundBuiltIn.java @@ -0,0 +1,46 @@ +/* + * 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.outputformat.MarkupOutputFormat; +import org.apache.freemarker.core.util._NullArgumentException; + +abstract class MarkupOutputFormatBoundBuiltIn extends SpecialBuiltIn { + + protected MarkupOutputFormat outputFormat; + + void bindToMarkupOutputFormat(MarkupOutputFormat outputFormat) { + _NullArgumentException.check(outputFormat); + this.outputFormat = outputFormat; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + if (outputFormat == null) { + // The parser should prevent this situation + throw new NullPointerException("outputFormat was null"); + } + return calculateResult(env); + } + + protected abstract TemplateModel calculateResult(Environment env) + throws TemplateException; + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java new file mode 100644 index 0000000..6a2bc2f --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MessageUtil.java @@ -0,0 +1,341 @@ +/* + * 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.ArrayList; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.valueformat.TemplateDateFormat; +import org.apache.freemarker.core.valueformat.TemplateNumberFormat; +import org.apache.freemarker.core.valueformat.TemplateValueFormatException; +import org.apache.freemarker.core.valueformat.UnknownDateTypeFormattingUnsupportedException; + +/** + * Utilities for creating error messages (and other messages). + */ +class MessageUtil { + + static final String UNKNOWN_DATE_TO_STRING_ERROR_MESSAGE + = "Can't convert the date-like value to string because it isn't " + + "known if it's a date (no time part), time or date-time value."; + + static final String UNKNOWN_DATE_TYPE_ERROR_TIP = + "Use ?date, ?time, or ?datetime to tell FreeMarker the exact type."; + + static final Object[] UNKNOWN_DATE_TO_STRING_TIPS = { + UNKNOWN_DATE_TYPE_ERROR_TIP, + "If you need a particular format only once, use ?string(pattern), like ?string('dd.MM.yyyy HH:mm:ss'), " + + "to specify which fields to display. " + }; + + static final String EMBEDDED_MESSAGE_BEGIN = "---begin-message---\n"; + + static final String EMBEDDED_MESSAGE_END = "\n---end-message---"; + + static final String ERROR_MESSAGE_HR = "----"; + + // Can't be instantiated + private MessageUtil() { } + + static String formatLocationForSimpleParsingError(Template template, int line, int column) { + return formatLocation("in", template, line, column); + } + + static String formatLocationForSimpleParsingError(String templateSourceOrLookupName, int line, int column) { + return formatLocation("in", templateSourceOrLookupName, line, column); + } + + static String formatLocationForEvaluationError(Template template, int line, int column) { + return formatLocation("at", template, line, column); + } + + static String formatLocationForEvaluationError(ASTDirMacro macro, int line, int column) { + Template t = macro.getTemplate(); + return formatLocation("at", t != null ? t.getSourceOrLookupName() : null, macro.getName(), macro.isFunction(), + line, column); + } + + private static String formatLocation(String preposition, Template template, int line, int column) { + return formatLocation(preposition, template != null ? template.getSourceOrLookupName() : null, line, column); + } + + private static String formatLocation(String preposition, String templateSourceName, int line, int column) { + return formatLocation( + preposition, templateSourceName, + null, false, + line, column); + } + + private static String formatLocation( + String preposition, String templateSourceName, + String macroOrFuncName, boolean isFunction, + int line, int column) { + String templateDesc; + if (line < 0) { + templateDesc = "?eval-ed string"; + macroOrFuncName = null; + } else { + templateDesc = templateSourceName != null + ? "template " + _StringUtil.jQuoteNoXSS(templateSourceName) + : "nameless template"; + } + return "in " + templateDesc + + (macroOrFuncName != null + ? " in " + (isFunction ? "function " : "macro ") + _StringUtil.jQuote(macroOrFuncName) + : "") + + " " + + preposition + " " + formatPosition(line, column); + } + + static String formatPosition(int line, int column) { + return "line " + (line >= 0 ? line : line - (ASTNode.RUNTIME_EVAL_LINE_DISPLACEMENT - 1)) + + ", column " + column; + } + + /** + * Returns a single line string that is no longer than {@code maxLength}. + * If will truncate the string at line-breaks too. + * The truncation is always signaled with a a {@code "..."} at the end of the result string. + */ + static String shorten(String s, int maxLength) { + if (maxLength < 5) maxLength = 5; + + boolean isTruncated = false; + + int brIdx = s.indexOf('\n'); + if (brIdx != -1) { + s = s.substring(0, brIdx); + isTruncated = true; + } + brIdx = s.indexOf('\r'); + if (brIdx != -1) { + s = s.substring(0, brIdx); + isTruncated = true; + } + + if (s.length() > maxLength) { + s = s.substring(0, maxLength - 3); + isTruncated = true; + } + + if (!isTruncated) { + return s; + } else { + if (s.endsWith(".")) { + if (s.endsWith("..")) { + if (s.endsWith("...")) { + return s; + } else { + return s + "."; + } + } else { + return s + ".."; + } + } else { + return s + "..."; + } + } + } + + static StringBuilder appendExpressionAsUntearable(StringBuilder sb, ASTExpression argExp) { + boolean needParen = + !(argExp instanceof ASTExpNumberLiteral) + && !(argExp instanceof ASTExpStringLiteral) + && !(argExp instanceof ASTExpBooleanLiteral) + && !(argExp instanceof ASTExpListLiteral) + && !(argExp instanceof ASTExpHashLiteral) + && !(argExp instanceof ASTExpVariable) + && !(argExp instanceof ASTExpDot) + && !(argExp instanceof ASTExpDynamicKeyName) + && !(argExp instanceof ASTExpMethodCall) + && !(argExp instanceof ASTExpBuiltIn); + if (needParen) sb.append('('); + sb.append(argExp.getCanonicalForm()); + if (needParen) sb.append(')'); + return sb; + } + + static TemplateModelException newArgCntError(String methodName, int argCnt, int expectedCnt) { + return newArgCntError(methodName, argCnt, expectedCnt, expectedCnt); + } + + static TemplateModelException newArgCntError(String methodName, int argCnt, int minCnt, int maxCnt) { + ArrayList/*<Object>*/ desc = new ArrayList(20); + + desc.add(methodName); + + desc.add("("); + if (maxCnt != 0) desc.add("..."); + desc.add(") expects "); + + if (minCnt == maxCnt) { + if (maxCnt == 0) { + desc.add("no"); + } else { + desc.add(Integer.valueOf(maxCnt)); + } + } else if (maxCnt - minCnt == 1) { + desc.add(Integer.valueOf(minCnt)); + desc.add(" or "); + desc.add(Integer.valueOf(maxCnt)); + } else { + desc.add(Integer.valueOf(minCnt)); + if (maxCnt != Integer.MAX_VALUE) { + desc.add(" to "); + desc.add(Integer.valueOf(maxCnt)); + } else { + desc.add(" or more (unlimited)"); + } + } + desc.add(" argument"); + if (maxCnt > 1) desc.add("s"); + + desc.add(" but has received "); + if (argCnt == 0) { + desc.add("none"); + } else { + desc.add(Integer.valueOf(argCnt)); + } + desc.add("."); + + return new _TemplateModelException(desc.toArray()); + } + + static TemplateModelException newMethodArgMustBeStringException(String methodName, int argIdx, TemplateModel arg) { + return newMethodArgUnexpectedTypeException(methodName, argIdx, "string", arg); + } + + static TemplateModelException newMethodArgMustBeNumberException(String methodName, int argIdx, TemplateModel arg) { + return newMethodArgUnexpectedTypeException(methodName, argIdx, "number", arg); + } + + static TemplateModelException newMethodArgMustBeBooleanException(String methodName, int argIdx, TemplateModel arg) { + return newMethodArgUnexpectedTypeException(methodName, argIdx, "boolean", arg); + } + + static TemplateModelException newMethodArgMustBeExtendedHashException( + String methodName, int argIdx, TemplateModel arg) { + return newMethodArgUnexpectedTypeException(methodName, argIdx, "extended hash", arg); + } + + static TemplateModelException newMethodArgMustBeSequenceException( + String methodName, int argIdx, TemplateModel arg) { + return newMethodArgUnexpectedTypeException(methodName, argIdx, "sequence", arg); + } + + static TemplateModelException newMethodArgMustBeSequenceOrCollectionException( + String methodName, int argIdx, TemplateModel arg) { + return newMethodArgUnexpectedTypeException(methodName, argIdx, "sequence or collection", arg); + } + + static TemplateModelException newMethodArgUnexpectedTypeException( + String methodName, int argIdx, String expectedType, TemplateModel arg) { + return new _TemplateModelException( + methodName, "(...) expects ", new _DelayedAOrAn(expectedType), " as argument #", Integer.valueOf(argIdx + 1), + ", but received ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(arg)), "."); + } + + /** + * The type of the argument was good, but it's value wasn't. + */ + static TemplateModelException newMethodArgInvalidValueException( + String methodName, int argIdx, Object... details) { + return new _TemplateModelException( + methodName, "(...) argument #", Integer.valueOf(argIdx + 1), + " had invalid value: ", details); + } + + /** + * The type of the argument was good, but the values of two or more arguments are inconsistent with each other. + */ + static TemplateModelException newMethodArgsInvalidValueException( + String methodName, Object... details) { + return new _TemplateModelException(methodName, "(...) arguments have invalid value: ", details); + } + + static TemplateException newInstantiatingClassNotAllowedException(String className, Environment env) { + return new _MiscTemplateException(env, + "Instantiating ", className, " is not allowed in the template for security reasons."); + } + + static TemplateModelException newCantFormatUnknownTypeDateException( + ASTExpression dateSourceExpr, UnknownDateTypeFormattingUnsupportedException cause) { + return new _TemplateModelException(cause, null, new _ErrorDescriptionBuilder( + MessageUtil.UNKNOWN_DATE_TO_STRING_ERROR_MESSAGE) + .blame(dateSourceExpr) + .tips(MessageUtil.UNKNOWN_DATE_TO_STRING_TIPS)); + } + + static TemplateException newCantFormatDateException(TemplateDateFormat format, ASTExpression dataSrcExp, + TemplateValueFormatException e, boolean useTempModelExc) { + _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( + "Failed to format date/time/datetime with format ", new _DelayedJQuote(format.getDescription()), ": ", + e.getMessage()) + .blame(dataSrcExp); + return useTempModelExc + ? new _TemplateModelException(e, null, desc) + : new _MiscTemplateException(e, null, desc); + } + + static TemplateException newCantFormatNumberException(TemplateNumberFormat format, ASTExpression dataSrcExp, + TemplateValueFormatException e, boolean useTempModelExc) { + _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder( + "Failed to format number with format ", new _DelayedJQuote(format.getDescription()), ": ", + e.getMessage()) + .blame(dataSrcExp); + return useTempModelExc + ? new _TemplateModelException(e, null, desc) + : new _MiscTemplateException(e, null, desc); + } + + /** + * @return "a" or "an" or "a(n)" (or "" for empty string) for an FTL type name + */ + static String getAOrAn(String s) { + if (s == null) return null; + if (s.length() == 0) return ""; + + char fc = Character.toLowerCase(s.charAt(0)); + if (fc == 'a' || fc == 'e' || fc == 'i') { + return "an"; + } else if (fc == 'h') { + String ls = s.toLowerCase(); + if (ls.startsWith("has") || ls.startsWith("hi")) { + return "a"; + } else if (ls.startsWith("ht")) { + return "an"; + } else { + return "a(n)"; + } + } else if (fc == 'u' || fc == 'o') { + return "a(n)"; + } else { + char sc = (s.length() > 1) ? s.charAt(1) : '\0'; + if (fc == 'x' && !(sc == 'a' || sc == 'e' || sc == 'i' || sc == 'a' || sc == 'o' || sc == 'u')) { + return "an"; + } else { + return "a"; + } + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java new file mode 100644 index 0000000..35d5943 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MiscUtil.java @@ -0,0 +1,69 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Utilities that didn't fit elsewhere. + */ +class MiscUtil { + + // Can't be instatiated + private MiscUtil() { } + + static final String C_FALSE = "false"; + static final String C_TRUE = "true"; + + /** + * Returns the map entries in source code order of the ASTExpression values. + */ + static List/*Map.Entry*/ sortMapOfExpressions(Map/*<?, ASTExpression>*/ map) { + ArrayList res = new ArrayList(map.entrySet()); + Collections.sort(res, + new Comparator() { // for sorting to source code order + @Override + public int compare(Object o1, Object o2) { + Map.Entry ent1 = (Map.Entry) o1; + ASTExpression exp1 = (ASTExpression) ent1.getValue(); + + Map.Entry ent2 = (Map.Entry) o2; + ASTExpression exp2 = (ASTExpression) ent2.getValue(); + + int res = exp1.beginLine - exp2.beginLine; + if (res != 0) return res; + res = exp1.beginColumn - exp2.beginColumn; + if (res != 0) return res; + + if (ent1 == ent2) return 0; + + // Should never reach this + return ((String) ent1.getKey()).compareTo((String) ent1.getKey()); + } + + }); + return res; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java new file mode 100644 index 0000000..00a387d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableParsingAndProcessingConfiguration.java @@ -0,0 +1,475 @@ +/* + * 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.InputStream; +import java.nio.charset.Charset; + +import org.apache.freemarker.core.outputformat.OutputFormat; +import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat; +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.util._NullArgumentException; + +public abstract class MutableParsingAndProcessingConfiguration< + SelfT extends MutableParsingAndProcessingConfiguration<SelfT>> + extends MutableProcessingConfiguration<SelfT> + implements ParsingAndProcessingConfiguration { + + private TemplateLanguage templateLanguage; + private Integer tagSyntax; + private Integer namingConvention; + private Boolean whitespaceStripping; + private Integer autoEscapingPolicy; + private Boolean recognizeStandardFileExtensions; + private OutputFormat outputFormat; + private Charset sourceEncoding; + private Integer tabSize; + + protected MutableParsingAndProcessingConfiguration() { + super(); + } + + /** + * Setter pair of {@link #getTagSyntax()}. + */ + public void setTagSyntax(int tagSyntax) { + valideTagSyntaxValue(tagSyntax); + this.tagSyntax = tagSyntax; + } + + // [FM3] Use enum; won't be needed + static void valideTagSyntaxValue(int tagSyntax) { + if (tagSyntax != ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX + && tagSyntax != ParsingConfiguration.SQUARE_BRACKET_TAG_SYNTAX + && tagSyntax != ParsingConfiguration.ANGLE_BRACKET_TAG_SYNTAX) { + throw new IllegalArgumentException( + "\"tagSyntax\" can only be set to one of these: " + + "Configuration.AUTO_DETECT_TAG_SYNTAX, Configuration.ANGLE_BRACKET_SYNTAX, " + + "or Configuration.SQUARE_BRACKET_SYNTAX"); + } + } + + /** + * Fluent API equivalent of {@link #tagSyntax(int)} + */ + public SelfT tagSyntax(int tagSyntax) { + setTagSyntax(tagSyntax); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetTagSyntax() { + this.tagSyntax = null; + } + + @Override + public int getTagSyntax() { + return isTagSyntaxSet() ? tagSyntax : getDefaultTagSyntax(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract int getDefaultTagSyntax(); + + @Override + public boolean isTagSyntaxSet() { + return tagSyntax != null; + } + + @Override + public TemplateLanguage getTemplateLanguage() { + return isTemplateLanguageSet() ? templateLanguage : getDefaultTemplateLanguage(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract TemplateLanguage getDefaultTemplateLanguage(); + + /** + * Setter pair of {@link #getTemplateLanguage()}. + */ + public void setTemplateLanguage(TemplateLanguage templateLanguage) { + _NullArgumentException.check("templateLanguage", templateLanguage); + this.templateLanguage = templateLanguage; + } + + /** + * Fluent API equivalent of {@link #setTemplateLanguage(TemplateLanguage)} + */ + public SelfT templateLanguage(TemplateLanguage templateLanguage) { + setTemplateLanguage(templateLanguage); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetTemplateLanguage() { + this.templateLanguage = null; + } + + @Override + public boolean isTemplateLanguageSet() { + return templateLanguage != null; + } + + /** + * Setter pair of {@link #getNamingConvention()}. + */ + public void setNamingConvention(int namingConvention) { + Configuration.validateNamingConventionValue(namingConvention); + this.namingConvention = namingConvention; + } + + /** + * Fluent API equivalent of {@link #setNamingConvention(int)} + */ + public SelfT namingConvention(int namingConvention) { + setNamingConvention(namingConvention); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetNamingConvention() { + this.namingConvention = null; + } + + /** + * The getter pair of {@link #setNamingConvention(int)}. + */ + @Override + public int getNamingConvention() { + return isNamingConventionSet() ? namingConvention + : getDefaultNamingConvention(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract int getDefaultNamingConvention(); + + /** + * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration.. + */ + @Override + public boolean isNamingConventionSet() { + return namingConvention != null; + } + + /** + * Setter pair of {@link ParsingConfiguration#getWhitespaceStripping()}. + */ + public void setWhitespaceStripping(boolean whitespaceStripping) { + this.whitespaceStripping = whitespaceStripping; + } + + /** + * Fluent API equivalent of {@link #setWhitespaceStripping(boolean)} + */ + public SelfT whitespaceStripping(boolean whitespaceStripping) { + setWhitespaceStripping(whitespaceStripping); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetWhitespaceStripping() { + this.whitespaceStripping = null; + } + + /** + * The getter pair of {@link #getWhitespaceStripping()}. + */ + @Override + public boolean getWhitespaceStripping() { + return isWhitespaceStrippingSet() ? whitespaceStripping : getDefaultWhitespaceStripping(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract boolean getDefaultWhitespaceStripping(); + + /** + * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration.. + */ + @Override + public boolean isWhitespaceStrippingSet() { + return whitespaceStripping != null; + } + + /** + * * Setter pair of {@link #getAutoEscapingPolicy()}. + */ + public void setAutoEscapingPolicy(int autoEscapingPolicy) { + validateAutoEscapingPolicyValue(autoEscapingPolicy); + this.autoEscapingPolicy = autoEscapingPolicy; + } + + // [FM3] Use enum; won't be needed + static void validateAutoEscapingPolicyValue(int autoEscapingPolicy) { + if (autoEscapingPolicy != ParsingConfiguration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY + && autoEscapingPolicy != ParsingConfiguration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY + && autoEscapingPolicy != ParsingConfiguration.DISABLE_AUTO_ESCAPING_POLICY) { + throw new IllegalArgumentException( + "\"tagSyntax\" can only be set to one of these: " + + "Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY," + + "Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, " + + "or Configuration.DISABLE_AUTO_ESCAPING_POLICY"); + } + } + + /** + * Fluent API equivalent of {@link #setAutoEscapingPolicy(int)} + */ + public SelfT autoEscapingPolicy(int autoEscapingPolicy) { + setAutoEscapingPolicy(autoEscapingPolicy); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetAutoEscapingPolicy() { + this.autoEscapingPolicy = null; + } + + /** + * The getter pair of {@link #setAutoEscapingPolicy(int)}. + */ + @Override + public int getAutoEscapingPolicy() { + return isAutoEscapingPolicySet() ? autoEscapingPolicy : getDefaultAutoEscapingPolicy(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract int getDefaultAutoEscapingPolicy(); + + /** + * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration.. + */ + @Override + public boolean isAutoEscapingPolicySet() { + return autoEscapingPolicy != null; + } + + /** + * Setter pair of {@link #getOutputFormat()}. + */ + public void setOutputFormat(OutputFormat outputFormat) { + if (outputFormat == null) { + throw new _NullArgumentException( + "outputFormat", + "You may meant: " + UndefinedOutputFormat.class.getSimpleName() + ".INSTANCE"); + } + this.outputFormat = outputFormat; + } + + /** + * Resets this setting to its initial state, as if it was never set. + */ + public void unsetOutputFormat() { + this.outputFormat = null; + } + + /** + * Fluent API equivalent of {@link #setOutputFormat(OutputFormat)} + */ + public SelfT outputFormat(OutputFormat outputFormat) { + setOutputFormat(outputFormat); + return self(); + } + + @Override + public OutputFormat getOutputFormat() { + return isOutputFormatSet() ? outputFormat : getDefaultOutputFormat(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract OutputFormat getDefaultOutputFormat(); + + /** + * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration.. + */ + @Override + public boolean isOutputFormatSet() { + return outputFormat != null; + } + + /** + * Setter pair of {@link ParsingConfiguration#getRecognizeStandardFileExtensions()}. + */ + public void setRecognizeStandardFileExtensions(boolean recognizeStandardFileExtensions) { + this.recognizeStandardFileExtensions = recognizeStandardFileExtensions; + } + + /** + * Fluent API equivalent of {@link #setRecognizeStandardFileExtensions(boolean)} + */ + public SelfT recognizeStandardFileExtensions(boolean recognizeStandardFileExtensions) { + setRecognizeStandardFileExtensions(recognizeStandardFileExtensions); + return self(); + } + + /** + * Resets this setting to its initial state, as if it was never set. + */ + public void unsetRecognizeStandardFileExtensions() { + recognizeStandardFileExtensions = null; + } + + /** + * Getter pair of {@link #setRecognizeStandardFileExtensions(boolean)}. + */ + @Override + public boolean getRecognizeStandardFileExtensions() { + return isRecognizeStandardFileExtensionsSet() ? recognizeStandardFileExtensions + : getDefaultRecognizeStandardFileExtensions(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract boolean getDefaultRecognizeStandardFileExtensions(); + + /** + * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration.. + */ + @Override + public boolean isRecognizeStandardFileExtensionsSet() { + return recognizeStandardFileExtensions != null; + } + + @Override + public Charset getSourceEncoding() { + return isSourceEncodingSet() ? sourceEncoding : getDefaultSourceEncoding(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract Charset getDefaultSourceEncoding(); + + /** + * The charset to be used when reading the template "file" that the {@link TemplateLoader} returns as binary + * ({@link InputStream}). If the {@code #ftl} header specifies an charset, that will override this. + */ + public void setSourceEncoding(Charset sourceEncoding) { + _NullArgumentException.check("sourceEncoding", sourceEncoding); + this.sourceEncoding = sourceEncoding; + } + + /** + * Fluent API equivalent of {@link #setSourceEncoding(Charset)} + */ + public SelfT sourceEncoding(Charset sourceEncoding) { + setSourceEncoding(sourceEncoding); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetSourceEncoding() { + this.sourceEncoding = null; + } + + @Override + public boolean isSourceEncodingSet() { + return sourceEncoding != null; + } + + /** + * Setter pair of {@link #getTabSize()}. + */ + public void setTabSize(int tabSize) { + if (tabSize < 1) { + throw new IllegalArgumentException("\"tabSize\" must be at least 1, but was " + tabSize); + } + // To avoid integer overflows: + if (tabSize > 256) { + throw new IllegalArgumentException("\"tabSize\" can't be more than 256, but was " + tabSize); + } + this.tabSize = Integer.valueOf(tabSize); + } + + /** + * Fluent API equivalent of {@link #setTabSize(int)} + */ + public SelfT tabSize(int tabSize) { + setTabSize(tabSize); + return self(); + } + + /** + * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another + * {@link ParsingConfiguration}). + */ + public void unsetTabSize() { + this.tabSize = null; + } + + @Override + public int getTabSize() { + return isTabSizeSet() ? tabSize.intValue() : getDefaultTabSize(); + } + + /** + * Returns the value the getter method returns when the setting is not set, possibly by inheriting the setting value + * from another {@link ParsingConfiguration}, or throws {@link SettingValueNotSetException}. + */ + protected abstract int getDefaultTabSize(); + + /** + * Tells if this setting is set directly in this object or its value is inherited from the parent parsing configuration.. + * + * @since 2.3.25 + */ + @Override + public boolean isTabSizeSet() { + return tabSize != null; + } + +}
