http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java new file mode 100644 index 0000000..cc96d81 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java @@ -0,0 +1,1068 @@ +/* + * 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.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.impl.DefaultObjectWrapper; +import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper; +import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat; +import org.apache.freemarker.core.outputformat.impl.PlainTextOutputFormat; +import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat; +import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat; +import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat; +import org.apache.freemarker.core.templateresolver.AndMatcher; +import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.FileExtensionMatcher; +import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher; +import org.apache.freemarker.core.templateresolver.FirstMatchTemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.MergingTemplateConfigurationFactory; +import org.apache.freemarker.core.templateresolver.NotMatcher; +import org.apache.freemarker.core.templateresolver.OrMatcher; +import org.apache.freemarker.core.templateresolver.PathGlobMatcher; +import org.apache.freemarker.core.templateresolver.PathRegexMatcher; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util.FTLUtil; +import org.apache.freemarker.core.util.GenericParseException; +import org.apache.freemarker.core.util._ClassUtil; +import org.apache.freemarker.core.util._StringUtil; + +/** + * Don't use this; used internally by FreeMarker, might changes without notice. + * + * Evaluates object builder expressions used in configuration {@link Properties}. + * It should be replaced with FTL later (when it was improved to be practical for this), so the syntax should be + * a subset of the future FTL syntax. This is also why this syntax is restrictive; it shouldn't accept anything that + * FTL will not. + */ +// Java 5: use generics for expectedClass +// Java 5: Introduce ObjectBuilder interface +public class _ObjectBuilderSettingEvaluator { + + private static final String INSTANCE_FIELD_NAME = "INSTANCE"; + + private static final String BUILD_METHOD_NAME = "build"; + + private static final String BUILDER_CLASS_POSTFIX_1 = "$Builder"; + private static final String BUILDER_CLASS_POSTFIX_2 = "Builder"; + + private static Map<String,String> SHORTHANDS; + + private static final Object VOID = new Object(); + + private final String src; + private final Class expectedClass; + private final boolean allowNull; + private final _SettingEvaluationEnvironment env; + + // Parser state: + private int pos; + + private _ObjectBuilderSettingEvaluator( + String src, int pos, Class expectedClass, boolean allowNull, _SettingEvaluationEnvironment env) { + this.src = src; + this.pos = pos; + this.expectedClass = expectedClass; + this.allowNull = allowNull; + this.env = env; + } + + public static Object eval(String src, Class expectedClass, boolean allowNull, _SettingEvaluationEnvironment env) + throws _ObjectBuilderSettingEvaluationException, + ClassNotFoundException, InstantiationException, IllegalAccessException { + return new _ObjectBuilderSettingEvaluator(src, 0, expectedClass, allowNull, env).eval(); + } + + /** + * Used for getting a list of setting assignments (like {@code (x=1, y=2)}) from an existing string, and apply it on + * an existing bean. + * + * @return The location of the next character to process. + */ + public static int configureBean( + String argumentListSrc, int posAfterOpenParen, Object bean, _SettingEvaluationEnvironment env) + throws _ObjectBuilderSettingEvaluationException, + ClassNotFoundException, InstantiationException, IllegalAccessException { + return new _ObjectBuilderSettingEvaluator( + argumentListSrc, posAfterOpenParen, bean.getClass(), true, env).configureBean(bean); + } + + private Object eval() throws _ObjectBuilderSettingEvaluationException, + ClassNotFoundException, InstantiationException, IllegalAccessException { + Object value; + + skipWS(); + value = ensureEvaled(fetchValue(false, true, false, true)); + skipWS(); + + if (pos != src.length()) { + throw new _ObjectBuilderSettingEvaluationException("end-of-expression", src, pos); + } + + if (value == null && !allowNull) { + throw new _ObjectBuilderSettingEvaluationException("Value can't be null."); + } + if (value != null && !expectedClass.isInstance(value)) { + throw new _ObjectBuilderSettingEvaluationException("The resulting object (of class " + + value.getClass() + ") is not a(n) " + expectedClass.getName() + "."); + } + + return value; + } + + private int configureBean(Object bean) throws _ObjectBuilderSettingEvaluationException, + ClassNotFoundException, InstantiationException, IllegalAccessException { + final PropertyAssignmentsExpression propAssignments = new PropertyAssignmentsExpression(bean); + fetchParameterListInto(propAssignments); + skipWS(); + propAssignments.eval(); + return pos; + } + + private Object ensureEvaled(Object value) throws _ObjectBuilderSettingEvaluationException { + return value instanceof SettingExpression ? ((SettingExpression) value).eval() : value; + } + + private Object fetchBuilderCall(boolean optional, boolean topLevel) + throws _ObjectBuilderSettingEvaluationException { + int startPos = pos; + + BuilderCallExpression exp = new BuilderCallExpression(); + // We need the canBeStaticField/mustBeStaticFiled complication to deal with legacy syntax where parentheses + // weren't required for constructor calls. + exp.canBeStaticField = true; + + final String fetchedClassName = fetchClassName(optional); + { + if (fetchedClassName == null) { + if (!optional) { + throw new _ObjectBuilderSettingEvaluationException("class name", src, pos); + } + return VOID; + } + exp.className = shorthandToFullQualified(fetchedClassName); + if (!fetchedClassName.equals(exp.className)) { + exp.canBeStaticField = false; + } + } + + skipWS(); + + char openParen = fetchOptionalChar("("); + // Only the top-level expression can omit the "(...)" + if (openParen == 0 && !topLevel) { + if (fetchedClassName.indexOf('.') != -1) { + exp.mustBeStaticField = true; + } else { + pos = startPos; + return VOID; + } + } + + if (openParen != 0) { + fetchParameterListInto(exp); + exp.canBeStaticField = false; + } + + return exp; + } + + private void fetchParameterListInto(ExpressionWithParameters exp) throws _ObjectBuilderSettingEvaluationException { + skipWS(); + if (fetchOptionalChar(")") != ')') { + do { + skipWS(); + + Object paramNameOrValue = fetchValue(false, false, true, false); + if (paramNameOrValue != VOID) { + skipWS(); + if (paramNameOrValue instanceof Name) { + exp.namedParamNames.add(((Name) paramNameOrValue).name); + + skipWS(); + fetchRequiredChar("="); + skipWS(); + + Object paramValue = fetchValue(false, false, true, true); + exp.namedParamValues.add(ensureEvaled(paramValue)); + } else { + if (!exp.namedParamNames.isEmpty()) { + throw new _ObjectBuilderSettingEvaluationException( + "Positional parameters must precede named parameters"); + } + if (!exp.getAllowPositionalParameters()) { + throw new _ObjectBuilderSettingEvaluationException( + "Positional parameters not supported here"); + } + exp.positionalParamValues.add(ensureEvaled(paramNameOrValue)); + } + + skipWS(); + } + } while (fetchRequiredChar(",)") == ','); + } + } + + private Object fetchValue(boolean optional, boolean topLevel, boolean resultCoerced, boolean resolveVariables) + throws _ObjectBuilderSettingEvaluationException { + if (pos < src.length()) { + Object val = fetchNumberLike(true, resultCoerced); + if (val != VOID) { + return val; + } + + val = fetchStringLiteral(true); + if (val != VOID) { + return val; + } + + val = fetchListLiteral(true); + if (val != VOID) { + return val; + } + + val = fetchMapLiteral(true); + if (val != VOID) { + return val; + } + + val = fetchBuilderCall(true, topLevel); + if (val != VOID) { + return val; + } + + String name = fetchSimpleName(true); + if (name != null) { + val = keywordToValueOrVoid(name); + if (val != VOID) { + return val; + } + + if (resolveVariables) { + // Not supported currently... + throw new _ObjectBuilderSettingEvaluationException("Can't resolve variable reference: " + name); + } else { + return new Name(name); + } + } + } + + if (optional) { + return VOID; + } else { + throw new _ObjectBuilderSettingEvaluationException("value or name", src, pos); + } + } + + private boolean isKeyword(String name) { + return keywordToValueOrVoid(name) != VOID; + } + + private Object keywordToValueOrVoid(String name) { + if (name.equals("true")) return Boolean.TRUE; + if (name.equals("false")) return Boolean.FALSE; + if (name.equals("null")) return null; + return VOID; + } + + private String fetchSimpleName(boolean optional) throws _ObjectBuilderSettingEvaluationException { + char c = pos < src.length() ? src.charAt(pos) : 0; + if (!isIdentifierStart(c)) { + if (optional) { + return null; + } else { + throw new _ObjectBuilderSettingEvaluationException("class name", src, pos); + } + } + int startPos = pos; + pos++; + + seekClassNameEnd: while (true) { + if (pos == src.length()) { + break seekClassNameEnd; + } + c = src.charAt(pos); + if (!isIdentifierMiddle(c)) { + break seekClassNameEnd; + } + pos++; + } + + return src.substring(startPos, pos); + } + + private String fetchClassName(boolean optional) throws _ObjectBuilderSettingEvaluationException { + int startPos = pos; + StringBuilder sb = new StringBuilder(); + do { + String name = fetchSimpleName(true); + if (name == null) { + if (!optional) { + throw new _ObjectBuilderSettingEvaluationException("name", src, pos); + } else { + pos = startPos; + return null; + } + } + sb.append(name); + + skipWS(); + + if (pos >= src.length() || src.charAt(pos) != '.') { + break; + } + sb.append('.'); + pos++; + + skipWS(); + } while (true); + + String className = sb.toString(); + if (isKeyword(className)) { + pos = startPos; + return null; + } + return className; + } + + private Object fetchNumberLike(boolean optional, boolean resultCoerced) + throws _ObjectBuilderSettingEvaluationException { + int startPos = pos; + boolean isVersion = false; + boolean hasDot = false; + seekTokenEnd: while (true) { + if (pos == src.length()) { + break seekTokenEnd; + } + char c = src.charAt(pos); + if (c == '.') { + if (hasDot) { + // More than one dot + isVersion = true; + } else { + hasDot = true; + } + } else if (!(isASCIIDigit(c) || c == '-')) { + break seekTokenEnd; + } + pos++; + } + + if (startPos == pos) { + if (optional) { + return VOID; + } else { + throw new _ObjectBuilderSettingEvaluationException("number-like", src, pos); + } + } + + String numStr = src.substring(startPos, pos); + if (isVersion) { + try { + return new Version(numStr); + } catch (IllegalArgumentException e) { + throw new _ObjectBuilderSettingEvaluationException("Malformed version number: " + numStr, e); + } + } else { + // For example, in 1.0f, numStr is "1.0", and typePostfix is "f". + String typePostfix = null; + seekTypePostfixEnd: while (true) { + if (pos == src.length()) { + break seekTypePostfixEnd; + } + char c = src.charAt(pos); + if (Character.isLetter(c)) { + if (typePostfix == null) { + typePostfix = String.valueOf(c); + } else { + typePostfix += c; + } + } else { + break seekTypePostfixEnd; + } + pos++; + } + + try { + if (numStr.endsWith(".")) { + throw new NumberFormatException("A number can't end with a dot"); + } + if (numStr.startsWith(".") || numStr.startsWith("-.") || numStr.startsWith("+.")) { + throw new NumberFormatException("A number can't start with a dot"); + } + + if (typePostfix == null) { + // Auto-detect type + if (numStr.indexOf('.') == -1) { + BigInteger biNum = new BigInteger(numStr); + final int bitLength = biNum.bitLength(); // Doesn't include sign bit + if (bitLength <= 31) { + return Integer.valueOf(biNum.intValue()); + } else if (bitLength <= 63) { + return Long.valueOf(biNum.longValue()); + } else { + return biNum; + } + } else { + if (resultCoerced) { + // The FTL way (BigDecimal is loseless, and it will be coerced to the target type later): + return new BigDecimal(numStr); + } else { + // The Java way (lossy but familiar): + return Double.valueOf(numStr); + } + } + } else { // Has explicitly specified type + if (typePostfix.equalsIgnoreCase("l")) { + return Long.valueOf(numStr); + } else if (typePostfix.equalsIgnoreCase("bi")) { + return new BigInteger(numStr); + } else if (typePostfix.equalsIgnoreCase("bd")) { + return new BigDecimal(numStr); + } else if (typePostfix.equalsIgnoreCase("d")) { + return Double.valueOf(numStr); + } else if (typePostfix.equalsIgnoreCase("f")) { + return Float.valueOf(numStr); + } else { + throw new _ObjectBuilderSettingEvaluationException( + "Unrecognized number type postfix: " + typePostfix); + } + } + + } catch (NumberFormatException e) { + throw new _ObjectBuilderSettingEvaluationException("Malformed number: " + numStr, e); + } + } + } + + private Object fetchStringLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException { + int startPos = pos; + char q = 0; + boolean afterEscape = false; + boolean raw = false; + seekTokenEnd: while (true) { + if (pos == src.length()) { + if (q != 0) { + // We had an open quotation + throw new _ObjectBuilderSettingEvaluationException(String.valueOf(q), src, pos); + } + break seekTokenEnd; + } + char c = src.charAt(pos); + if (q == 0) { + if (c == 'r' && (pos + 1 < src.length())) { + // Maybe it's like r"foo\bar" + raw = true; + c = src.charAt(pos + 1); + } + if (c == '\'') { + q = '\''; + } else if (c == '"') { + q = '"'; + } else { + break seekTokenEnd; + } + if (raw) { + // because of the preceding "r" + pos++; + } + } else { + if (!afterEscape) { + if (c == '\\' && !raw) { + afterEscape = true; + } else if (c == q) { + break seekTokenEnd; + } else if (c == '{') { + char prevC = src.charAt(pos - 1); + if (prevC == '$' || prevC == '#') { + throw new _ObjectBuilderSettingEvaluationException( + "${...} and #{...} aren't allowed here."); + } + } + } else { + afterEscape = false; + } + } + pos++; + } + if (startPos == pos) { + if (optional) { + return VOID; + } else { + throw new _ObjectBuilderSettingEvaluationException("string literal", src, pos); + } + } + + final String sInside = src.substring(startPos + (raw ? 2 : 1), pos); + try { + pos++; // skip closing quotation mark + return raw ? sInside : FTLUtil.unescapeStringLiteralPart(sInside); + } catch (GenericParseException e) { + throw new _ObjectBuilderSettingEvaluationException("Malformed string literal: " + sInside, e); + } + } + + private Object fetchListLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException { + if (pos == src.length() || src.charAt(pos) != '[') { + if (!optional) { + throw new _ObjectBuilderSettingEvaluationException("[", src, pos); + } + return VOID; + } + pos++; + + ListExpression listExp = new ListExpression(); + + while (true) { + skipWS(); + + if (fetchOptionalChar("]") != 0) { + return listExp; + } + if (listExp.itemCount() != 0) { + fetchRequiredChar(","); + skipWS(); + } + + listExp.addItem(fetchValue(false, false, false, true)); + + skipWS(); + } + } + + private Object fetchMapLiteral(boolean optional) throws _ObjectBuilderSettingEvaluationException { + if (pos == src.length() || src.charAt(pos) != '{') { + if (!optional) { + throw new _ObjectBuilderSettingEvaluationException("{", src, pos); + } + return VOID; + } + pos++; + + MapExpression mapExp = new MapExpression(); + + while (true) { + skipWS(); + + if (fetchOptionalChar("}") != 0) { + return mapExp; + } + if (mapExp.itemCount() != 0) { + fetchRequiredChar(","); + skipWS(); + } + + Object key = fetchValue(false, false, false, true); + skipWS(); + fetchRequiredChar(":"); + skipWS(); + Object value = fetchValue(false, false, false, true); + mapExp.addItem(new KeyValuePair(key, value)); + + skipWS(); + } + } + + private void skipWS() { + while (true) { + if (pos == src.length()) { + return; + } + char c = src.charAt(pos); + if (!Character.isWhitespace(c)) { + return; + } + pos++; + } + } + + private char fetchOptionalChar(String expectedChars) throws _ObjectBuilderSettingEvaluationException { + return fetchChar(expectedChars, true); + } + + private char fetchRequiredChar(String expectedChars) throws _ObjectBuilderSettingEvaluationException { + return fetchChar(expectedChars, false); + } + + private char fetchChar(String expectedChars, boolean optional) throws _ObjectBuilderSettingEvaluationException { + char c = pos < src.length() ? src.charAt(pos) : 0; + if (expectedChars.indexOf(c) != -1) { + pos++; + return c; + } else if (optional) { + return 0; + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < expectedChars.length(); i++) { + if (i != 0) { + sb.append(" or "); + } + sb.append(_StringUtil.jQuote(expectedChars.substring(i, i + 1))); + } + throw new _ObjectBuilderSettingEvaluationException( + sb.toString(), + src, pos); + } + } + + private boolean isASCIIDigit(char c) { + return c >= '0' && c <= '9'; + } + + private boolean isIdentifierStart(char c) { + return Character.isLetter(c) || c == '_' || c == '$'; + } + + private boolean isIdentifierMiddle(char c) { + return isIdentifierStart(c) || isASCIIDigit(c); + } + + private static synchronized String shorthandToFullQualified(String className) { + if (SHORTHANDS == null) { + SHORTHANDS = new HashMap/*<String,String>*/(); + + addWithSimpleName(SHORTHANDS, DefaultObjectWrapper.class); + addWithSimpleName(SHORTHANDS, DefaultObjectWrapper.class); + addWithSimpleName(SHORTHANDS, RestrictedObjectWrapper.class); + + addWithSimpleName(SHORTHANDS, TemplateConfiguration.class); + + addWithSimpleName(SHORTHANDS, PathGlobMatcher.class); + addWithSimpleName(SHORTHANDS, FileNameGlobMatcher.class); + addWithSimpleName(SHORTHANDS, FileExtensionMatcher.class); + addWithSimpleName(SHORTHANDS, PathRegexMatcher.class); + addWithSimpleName(SHORTHANDS, AndMatcher.class); + addWithSimpleName(SHORTHANDS, OrMatcher.class); + addWithSimpleName(SHORTHANDS, NotMatcher.class); + + addWithSimpleName(SHORTHANDS, ConditionalTemplateConfigurationFactory.class); + addWithSimpleName(SHORTHANDS, MergingTemplateConfigurationFactory.class); + addWithSimpleName(SHORTHANDS, FirstMatchTemplateConfigurationFactory.class); + + addWithSimpleName(SHORTHANDS, HTMLOutputFormat.class); + addWithSimpleName(SHORTHANDS, XMLOutputFormat.class); + addWithSimpleName(SHORTHANDS, RTFOutputFormat.class); + addWithSimpleName(SHORTHANDS, PlainTextOutputFormat.class); + addWithSimpleName(SHORTHANDS, UndefinedOutputFormat.class); + + addWithSimpleName(SHORTHANDS, TemplateLanguage.class); + + addWithSimpleName(SHORTHANDS, Locale.class); + + { + String tzbClassName = _TimeZoneBuilder.class.getName(); + SHORTHANDS.put("TimeZone", + tzbClassName.substring(0, tzbClassName.length() - BUILDER_CLASS_POSTFIX_2.length())); + } + + { + String csClassName = _CharsetBuilder.class.getName(); + SHORTHANDS.put("Charset", + csClassName.substring(0, csClassName.length() - BUILDER_CLASS_POSTFIX_2.length())); + } + + // For accessing static fields: + addWithSimpleName(SHORTHANDS, Configuration.class); + } + String fullClassName = SHORTHANDS.get(className); + return fullClassName == null ? className : fullClassName; + } + + private static void addWithSimpleName(Map map, Class<?> pClass) { + map.put(pClass.getSimpleName(), pClass.getName()); + } + + private void setJavaBeanProperties(Object bean, + List/*<String>*/ namedParamNames, List/*<Object>*/ namedParamValues) + throws _ObjectBuilderSettingEvaluationException { + if (namedParamNames.isEmpty()) { + return; + } + + final Class cl = bean.getClass(); + Map/*<String,Method>*/ beanPropSetters; + try { + PropertyDescriptor[] propDescs = Introspector.getBeanInfo(cl).getPropertyDescriptors(); + beanPropSetters = new HashMap(propDescs.length * 4 / 3, 1.0f); + for (PropertyDescriptor propDesc : propDescs) { + final Method writeMethod = propDesc.getWriteMethod(); + if (writeMethod != null) { + beanPropSetters.put(propDesc.getName(), writeMethod); + } + } + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException("Failed to inspect " + cl.getName() + " class", e); + } + + TemplateHashModel beanTM = null; + for (int i = 0; i < namedParamNames.size(); i++) { + String name = (String) namedParamNames.get(i); + if (!beanPropSetters.containsKey(name)) { + throw new _ObjectBuilderSettingEvaluationException( + "The " + cl.getName() + " class has no writeable JavaBeans property called " + + _StringUtil.jQuote(name) + "."); + } + + Method beanPropSetter = (Method) beanPropSetters.put(name, null); + if (beanPropSetter == null) { + throw new _ObjectBuilderSettingEvaluationException( + "JavaBeans property " + _StringUtil.jQuote(name) + " is set twice."); + } + + try { + if (beanTM == null) { + TemplateModel wrappedObj = env.getObjectWrapper().wrap(bean); + if (!(wrappedObj instanceof TemplateHashModel)) { + throw new _ObjectBuilderSettingEvaluationException( + "The " + cl.getName() + " class is not a wrapped as TemplateHashModel."); + } + beanTM = (TemplateHashModel) wrappedObj; + } + + TemplateModel m = beanTM.get(beanPropSetter.getName()); + if (m == null) { + throw new _ObjectBuilderSettingEvaluationException( + "Can't find " + beanPropSetter + " as FreeMarker method."); + } + if (!(m instanceof TemplateMethodModelEx)) { + throw new _ObjectBuilderSettingEvaluationException( + _StringUtil.jQuote(beanPropSetter.getName()) + " wasn't a TemplateMethodModelEx."); + } + List/*TemplateModel*/ args = new ArrayList(); + args.add(env.getObjectWrapper().wrap(namedParamValues.get(i))); + ((TemplateMethodModelEx) m).exec(args); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to set " + _StringUtil.jQuote(name), e); + } + } + } + + private static class Name { + + public Name(String name) { + this.name = name; + } + + private final String name; + } + + private abstract static class SettingExpression { + abstract Object eval() throws _ObjectBuilderSettingEvaluationException; + } + + private abstract class ExpressionWithParameters extends SettingExpression { + protected List positionalParamValues = new ArrayList(); + protected List/*<String>*/ namedParamNames = new ArrayList(); + protected List/*<Object>*/ namedParamValues = new ArrayList(); + + protected abstract boolean getAllowPositionalParameters(); + } + + private class ListExpression extends SettingExpression { + + private List<Object> items = new ArrayList(); + + void addItem(Object item) { + items.add(item); + } + + public int itemCount() { + return items.size(); + } + + @Override + Object eval() throws _ObjectBuilderSettingEvaluationException { + ArrayList res = new ArrayList(items.size()); + for (Object item : items) { + res.add(ensureEvaled(item)); + } + return res; + } + + } + + private class MapExpression extends SettingExpression { + + private List<KeyValuePair> items = new ArrayList(); + + void addItem(KeyValuePair item) { + items.add(item); + } + + public int itemCount() { + return items.size(); + } + + @Override + Object eval() throws _ObjectBuilderSettingEvaluationException { + LinkedHashMap res = new LinkedHashMap(items.size() * 4 / 3, 1f); + for (KeyValuePair item : items) { + Object key = ensureEvaled(item.key); + if (key == null) { + throw new _ObjectBuilderSettingEvaluationException("Map can't use null as key."); + } + res.put(key, ensureEvaled(item.value)); + } + return res; + } + + } + + private static class KeyValuePair { + private final Object key; + private final Object value; + + public KeyValuePair(Object key, Object value) { + this.key = key; + this.value = value; + } + } + + private class BuilderCallExpression extends ExpressionWithParameters { + private String className; + private boolean canBeStaticField; + private boolean mustBeStaticField; + + @Override + Object eval() throws _ObjectBuilderSettingEvaluationException { + if (mustBeStaticField) { + if (!canBeStaticField) { + throw new BugException(); + } + return getStaticFieldValue(className); + } + + Class cl; + + boolean clIsBuilderClass; + try { + cl = _ClassUtil.forName(className + BUILDER_CLASS_POSTFIX_1); + clIsBuilderClass = true; + } catch (ClassNotFoundException eIgnored) { + try { + cl = _ClassUtil.forName(className + BUILDER_CLASS_POSTFIX_2); + clIsBuilderClass = true; + } catch (ClassNotFoundException e) { + clIsBuilderClass = false; + try { + cl = _ClassUtil.forName(className); + } catch (Exception e2) { + boolean failedToGetAsStaticField; + if (canBeStaticField) { + // Try to interpret className as static filed: + try { + return getStaticFieldValue(className); + } catch (_ObjectBuilderSettingEvaluationException e3) { + // Suppress it + failedToGetAsStaticField = true; + } + } else { + failedToGetAsStaticField = false; + } + throw new _ObjectBuilderSettingEvaluationException( + "Failed to get class " + _StringUtil.jQuote(className) + + (failedToGetAsStaticField ? " (also failed to resolve name as static field)" : "") + + ".", + e2); + } + } + } + + if (!clIsBuilderClass && hasNoParameters()) { + try { + Field f = cl.getField(INSTANCE_FIELD_NAME); + if ((f.getModifiers() & (Modifier.PUBLIC | Modifier.STATIC)) + == (Modifier.PUBLIC | Modifier.STATIC)) { + return f.get(null); + } + } catch (NoSuchFieldException e) { + // Expected + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Error when trying to access " + _StringUtil.jQuote(className) + "." + + INSTANCE_FIELD_NAME, e); + } + } + + // Create the object to return or its builder: + Object constructorResult = callConstructor(cl); + + // Named parameters will set JavaBeans properties: + setJavaBeanProperties(constructorResult, namedParamNames, namedParamValues); + + return clIsBuilderClass ? callBuild(constructorResult) : constructorResult; + } + + private Object getStaticFieldValue(String dottedName) throws _ObjectBuilderSettingEvaluationException { + int lastDotIdx = dottedName.lastIndexOf('.'); + if (lastDotIdx == -1) { + throw new IllegalArgumentException(); + } + String className = shorthandToFullQualified(dottedName.substring(0, lastDotIdx)); + String fieldName = dottedName.substring(lastDotIdx + 1); + + Class<?> cl; + try { + cl = _ClassUtil.forName(className); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to get field's parent class, " + _StringUtil.jQuote(className) + ".", + e); + } + + Field field; + try { + field = cl.getField(fieldName); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to get field " + _StringUtil.jQuote(fieldName) + " from class " + + _StringUtil.jQuote(className) + ".", + e); + } + + if ((field.getModifiers() & Modifier.STATIC) == 0) { + throw new _ObjectBuilderSettingEvaluationException("Referred field isn't static: " + field); + } + if ((field.getModifiers() & Modifier.PUBLIC) == 0) { + throw new _ObjectBuilderSettingEvaluationException("Referred field isn't public: " + field); + } + + if (field.getName().equals(INSTANCE_FIELD_NAME)) { + throw new _ObjectBuilderSettingEvaluationException( + "The " + INSTANCE_FIELD_NAME + " field is only accessible through pseudo-constructor call: " + + className + "()"); + } + + try { + return field.get(null); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException("Failed to get field value: " + field, e); + } + } + + private Object callConstructor(Class cl) + throws _ObjectBuilderSettingEvaluationException { + if (hasNoParameters()) { + // No need to invoke ObjectWrapper + try { + return cl.newInstance(); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to call " + cl.getName() + " 0-argument constructor", e); + } + } else { + DefaultObjectWrapper ow = env.getObjectWrapper(); + List/*<TemplateModel>*/ tmArgs = new ArrayList(positionalParamValues.size()); + for (int i = 0; i < positionalParamValues.size(); i++) { + try { + tmArgs.add(ow.wrap(positionalParamValues.get(i))); + } catch (TemplateModelException e) { + throw new _ObjectBuilderSettingEvaluationException("Failed to wrap arg #" + (i + 1), e); + } + } + try { + return ow.newInstance(cl, tmArgs); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException( + "Failed to call " + cl.getName() + " constructor", e); + } + } + } + + private Object callBuild(Object constructorResult) + throws _ObjectBuilderSettingEvaluationException { + final Class cl = constructorResult.getClass(); + Method buildMethod; + try { + buildMethod = constructorResult.getClass().getMethod(BUILD_METHOD_NAME, (Class[]) null); + } catch (NoSuchMethodException e) { + throw new _ObjectBuilderSettingEvaluationException("The " + cl.getName() + + " builder class must have a public " + BUILD_METHOD_NAME + "() method", e); + } catch (Exception e) { + throw new _ObjectBuilderSettingEvaluationException("Failed to get the " + BUILD_METHOD_NAME + + "() method of the " + cl.getName() + " builder class", e); + } + + try { + return buildMethod.invoke(constructorResult, (Object[]) null); + } catch (Exception e) { + Throwable cause; + if (e instanceof InvocationTargetException) { + cause = ((InvocationTargetException) e).getTargetException(); + } else { + cause = e; + } + throw new _ObjectBuilderSettingEvaluationException("Failed to call " + BUILD_METHOD_NAME + + "() method on " + cl.getName() + " instance", cause); + } + } + + private boolean hasNoParameters() { + return positionalParamValues.isEmpty() && namedParamValues.isEmpty(); + } + + @Override + protected boolean getAllowPositionalParameters() { + return true; + } + + } + + private class PropertyAssignmentsExpression extends ExpressionWithParameters { + + private final Object bean; + + public PropertyAssignmentsExpression(Object bean) { + this.bean = bean; + } + + @Override + Object eval() throws _ObjectBuilderSettingEvaluationException { + setJavaBeanProperties(bean, namedParamNames, namedParamValues); + return bean; + } + + @Override + protected boolean getAllowPositionalParameters() { + return false; + } + + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java new file mode 100644 index 0000000..9501185 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_SettingEvaluationEnvironment.java @@ -0,0 +1,61 @@ +/* + * 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.Properties; + +import org.apache.freemarker.core.model.impl.DefaultObjectWrapper; + +/** + * Don't use this; used internally by FreeMarker, might changes without notice. + * The runtime environment used during the evaluation of configuration {@link Properties}. + */ +public class _SettingEvaluationEnvironment { + + private static final ThreadLocal CURRENT = new ThreadLocal(); + + private DefaultObjectWrapper objectWrapper; + + public static _SettingEvaluationEnvironment getCurrent() { + Object r = CURRENT.get(); + if (r != null) { + return (_SettingEvaluationEnvironment) r; + } + return new _SettingEvaluationEnvironment(); + } + + public static _SettingEvaluationEnvironment startScope() { + Object previous = CURRENT.get(); + CURRENT.set(new _SettingEvaluationEnvironment()); + return (_SettingEvaluationEnvironment) previous; + } + + public static void endScope(_SettingEvaluationEnvironment previous) { + CURRENT.set(previous); + } + + public DefaultObjectWrapper getObjectWrapper() { + if (objectWrapper == null) { + objectWrapper = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build(); + } + return objectWrapper; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java new file mode 100644 index 0000000..76e9d2b --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_TemplateModelException.java @@ -0,0 +1,133 @@ +/* + * 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.TemplateModelException; +import org.apache.freemarker.core.util._ClassUtil; + +public class _TemplateModelException extends TemplateModelException { + + // Note: On Java 5 we will use `String descPart1, Object... furtherDescParts` instead of `Object[] descriptionParts` + // and `String description`. That's why these are at the end of the parameter list. + + // ----------------------------------------------------------------------------------------------------------------- + // Permutation group: + + public _TemplateModelException(String description) { + super(description); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Permutation group: + + public _TemplateModelException(Throwable cause, String description) { + this(cause, null, description); + } + + public _TemplateModelException(Environment env, String description) { + this((Throwable) null, env, description); + } + + public _TemplateModelException(Throwable cause, Environment env) { + this(cause, env, (String) null); + } + + public _TemplateModelException(Throwable cause) { + this(cause, null, (String) null); + } + + public _TemplateModelException(Throwable cause, Environment env, String description) { + super(cause, env, description, true); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Permutation group: + + public _TemplateModelException(_ErrorDescriptionBuilder description) { + this(null, description); + } + + public _TemplateModelException(Environment env, _ErrorDescriptionBuilder description) { + this(null, env, description); + } + + public _TemplateModelException(Throwable cause, Environment env, _ErrorDescriptionBuilder description) { + super(cause, env, description, true); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Permutation group: + + public _TemplateModelException(Object... descriptionParts) { + this((Environment) null, descriptionParts); + } + + public _TemplateModelException(Environment env, Object... descriptionParts) { + this((Throwable) null, env, descriptionParts); + } + + public _TemplateModelException(Throwable cause, Object... descriptionParts) { + this(cause, null, descriptionParts); + } + + public _TemplateModelException(Throwable cause, Environment env, Object... descriptionParts) { + super(cause, env, new _ErrorDescriptionBuilder(descriptionParts), true); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Permutation group: + + public _TemplateModelException(ASTExpression blamed, Object... descriptionParts) { + this(blamed, null, descriptionParts); + } + + public _TemplateModelException(ASTExpression blamed, Environment env, Object... descriptionParts) { + this(blamed, null, env, descriptionParts); + } + + public _TemplateModelException(ASTExpression blamed, Throwable cause, Environment env, Object... descriptionParts) { + super(cause, env, new _ErrorDescriptionBuilder(descriptionParts).blame(blamed), true); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Permutation group: + + public _TemplateModelException(ASTExpression blamed, String description) { + this(blamed, null, description); + } + + public _TemplateModelException(ASTExpression blamed, Environment env, String description) { + this(blamed, null, env, description); + } + + public _TemplateModelException(ASTExpression blamed, Throwable cause, Environment env, String description) { + super(cause, env, new _ErrorDescriptionBuilder(description).blame(blamed), true); + } + + static Object[] modelHasStoredNullDescription(Class expected, TemplateModel model) { + return new Object[] { + "The FreeMarker value exists, but has nothing inside it; the TemplateModel object (class: ", + model.getClass().getName(), ") has returned a null", + (expected != null ? new Object[] { " instead of a ", _ClassUtil.getShortClassName(expected) } : ""), + ". This is possibly a bug in the non-FreeMarker code that builds the data-model." }; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java new file mode 100644 index 0000000..b923b3c --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_TimeZoneBuilder.java @@ -0,0 +1,43 @@ +/* + * 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.TimeZone; + +/** + * For internal use only; don't depend on this, there's no backward compatibility guarantee at all! + */ +public class _TimeZoneBuilder { + + private final String timeZoneId; + + public _TimeZoneBuilder(String timeZoneId) { + this.timeZoneId = timeZoneId; + } + + public TimeZone build() { + TimeZone timeZone = TimeZone.getTimeZone(timeZoneId); + if (timeZone.getID().equals("GMT") && !timeZoneId.equals("GMT") && !timeZoneId.equals("UTC") + && !timeZoneId.equals("GMT+00") && !timeZoneId.equals("GMT+00:00") && !timeZoneId.equals("GMT+0000")) { + throw new IllegalArgumentException("Unrecognized time zone: " + timeZoneId); + } + return timeZone; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.java new file mode 100644 index 0000000..56481b8 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_UnexpectedTypeErrorExplainerTemplateModel.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 org.apache.freemarker.core.model.TemplateModel; + +/** + * Don't use this; used internally by FreeMarker, might changes without notice. + * + * <p>Implemented by {@link TemplateModel}-s that can explain why they don't implement a certain type. + * */ +public interface _UnexpectedTypeErrorExplainerTemplateModel extends TemplateModel { + + /** + * @return A single {@link _ErrorDescriptionBuilder} tip, or {@code null}. + */ + Object[] explainTypeError(Class[]/*<? extends TemplateModel>*/ expectedClasses); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java new file mode 100644 index 0000000..afe22be --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.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.arithmetic; + +import java.math.BigDecimal; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.TemplateException; + +/** + * Implements the arithmetic operations executed by the template language; see + * {@link Configuration#getArithmeticEngine()}. + */ +public abstract class ArithmeticEngine { + + public abstract int compareNumbers(Number first, Number second) throws TemplateException; + public abstract Number add(Number first, Number second) throws TemplateException; + public abstract Number subtract(Number first, Number second) throws TemplateException; + public abstract Number multiply(Number first, Number second) throws TemplateException; + public abstract Number divide(Number first, Number second) throws TemplateException; + public abstract Number modulus(Number first, Number second) throws TemplateException; + // [FM3] Add negate (should keep the Number type even for BigDecimalArithmeticEngine, unlike multiply). Then fix + // the negate operation in the template language. + + /** + * Should be able to parse all FTL numerical literals, Java Double toString results, and XML Schema numbers. + * This means these should be parsed successfully, except if the arithmetical engine + * couldn't support the resulting value anyway (such as NaN, infinite, even non-integers): + * {@code -123.45}, {@code 1.5e3}, {@code 1.5E3}, {@code 0005}, {@code +0}, {@code -0}, {@code NaN}, + * {@code INF}, {@code -INF}, {@code Infinity}, {@code -Infinity}. + */ + public abstract Number toNumber(String s); + + protected int minScale = 12; + protected int maxScale = 12; + protected int roundingPolicy = BigDecimal.ROUND_HALF_UP; + + /** + * Sets the minimal scale to use when dividing BigDecimal numbers. Default + * value is 12. + */ + public void setMinScale(int minScale) { + if (minScale < 0) { + throw new IllegalArgumentException("minScale < 0"); + } + this.minScale = minScale; + } + + /** + * Sets the maximal scale to use when multiplying BigDecimal numbers. + * Default value is 100. + */ + public void setMaxScale(int maxScale) { + if (maxScale < minScale) { + throw new IllegalArgumentException("maxScale < minScale"); + } + this.maxScale = maxScale; + } + + public void setRoundingPolicy(int roundingPolicy) { + if (roundingPolicy != BigDecimal.ROUND_CEILING + && roundingPolicy != BigDecimal.ROUND_DOWN + && roundingPolicy != BigDecimal.ROUND_FLOOR + && roundingPolicy != BigDecimal.ROUND_HALF_DOWN + && roundingPolicy != BigDecimal.ROUND_HALF_EVEN + && roundingPolicy != BigDecimal.ROUND_HALF_UP + && roundingPolicy != BigDecimal.ROUND_UNNECESSARY + && roundingPolicy != BigDecimal.ROUND_UP) { + throw new IllegalArgumentException("invalid rounding policy"); + } + + this.roundingPolicy = roundingPolicy; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java new file mode 100644 index 0000000..b022f74 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java @@ -0,0 +1,107 @@ +/* + * 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.arithmetic.impl; + +import java.math.BigDecimal; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.util._NumberUtil; + +/** + * Arithmetic engine that converts all numbers to {@link BigDecimal} and + * then operates on them. This is FreeMarker's default arithmetic engine. + */ +public class BigDecimalArithmeticEngine extends ArithmeticEngine { + + public static final BigDecimalArithmeticEngine INSTANCE = new BigDecimalArithmeticEngine(); + + protected BigDecimalArithmeticEngine() { + // + } + + @Override + public int compareNumbers(Number first, Number second) { + // We try to find the result based on the sign (+/-/0) first, because: + // - It's much faster than converting to BigDecial, and comparing to 0 is the most common comparison. + // - It doesn't require any type conversions, and thus things like "Infinity > 0" won't fail. + int firstSignum = _NumberUtil.getSignum(first); + int secondSignum = _NumberUtil.getSignum(second); + if (firstSignum != secondSignum) { + return firstSignum < secondSignum ? -1 : (firstSignum > secondSignum ? 1 : 0); + } else if (firstSignum == 0 && secondSignum == 0) { + return 0; + } else { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return left.compareTo(right); + } + } + + @Override + public Number add(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return left.add(right); + } + + @Override + public Number subtract(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return left.subtract(right); + } + + @Override + public Number multiply(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + BigDecimal result = left.multiply(right); + if (result.scale() > maxScale) { + result = result.setScale(maxScale, roundingPolicy); + } + return result; + } + + @Override + public Number divide(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return divide(left, right); + } + + @Override + public Number modulus(Number first, Number second) { + long left = first.longValue(); + long right = second.longValue(); + return Long.valueOf(left % right); + } + + @Override + public Number toNumber(String s) { + return _NumberUtil.toBigDecimalOrDouble(s); + } + + private BigDecimal divide(BigDecimal left, BigDecimal right) { + int scale1 = left.scale(); + int scale2 = right.scale(); + int scale = Math.max(scale1, scale2); + scale = Math.max(minScale, scale); + return left.divide(right, scale, roundingPolicy); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java new file mode 100644 index 0000000..12c27d9 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java @@ -0,0 +1,381 @@ +/* + * 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.arithmetic.impl; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core._MiscTemplateException; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._NumberUtil; + +/** + * Arithmetic engine that uses (more-or-less) the widening conversions of + * Java language to determine the type of result of operation, instead of + * converting everything to BigDecimal up front. + * <p> + * Widening conversions occur in following situations: + * <ul> + * <li>byte and short are always widened to int (alike to Java language).</li> + * <li>To preserve magnitude: when operands are of different types, the + * result type is the type of the wider operand.</li> + * <li>to avoid overflows: if add, subtract, or multiply would overflow on + * integer types, the result is widened from int to long, or from long to + * BigInteger.</li> + * <li>to preserve fractional part: if a division of integer types would + * have a fractional part, int and long are converted to double, and + * BigInteger is converted to BigDecimal. An operation on a float and a + * long results in a double. An operation on a float or double and a + * BigInteger results in a BigDecimal.</li> + * </ul> + */ +// [FM3] Review +public class ConservativeArithmeticEngine extends ArithmeticEngine { + + public static final ConservativeArithmeticEngine INSTANCE = new ConservativeArithmeticEngine(); + + private static final int INTEGER = 0; + private static final int LONG = 1; + private static final int FLOAT = 2; + private static final int DOUBLE = 3; + private static final int BIG_INTEGER = 4; + private static final int BIG_DECIMAL = 5; + + private static final Map classCodes = createClassCodesMap(); + + protected ConservativeArithmeticEngine() { + // + } + + @Override + public int compareNumbers(Number first, Number second) throws TemplateException { + switch (getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case FLOAT: { + float n1 = first.floatValue(); + float n2 = second.floatValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case DOUBLE: { + double n1 = first.doubleValue(); + double n2 = second.doubleValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.compareTo(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + return n1.compareTo(n2); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number add(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + int n = n1 + n2; + return + ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check + ? Long.valueOf(((long) n1) + n2) + : Integer.valueOf(n); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + long n = n1 + n2; + return + ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check + ? toBigInteger(first).add(toBigInteger(second)) + : Long.valueOf(n); + } + case FLOAT: { + return Float.valueOf(first.floatValue() + second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() + second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.add(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + return n1.add(n2); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number subtract(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + int n = n1 - n2; + return + ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check + ? Long.valueOf(((long) n1) - n2) + : Integer.valueOf(n); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + long n = n1 - n2; + return + ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check + ? toBigInteger(first).subtract(toBigInteger(second)) + : Long.valueOf(n); + } + case FLOAT: { + return Float.valueOf(first.floatValue() - second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() - second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.subtract(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + return n1.subtract(n2); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number multiply(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + int n = n1 * n2; + return + n1 == 0 || n / n1 == n2 // overflow check + ? Integer.valueOf(n) + : Long.valueOf(((long) n1) * n2); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + long n = n1 * n2; + return + n1 == 0L || n / n1 == n2 // overflow check + ? Long.valueOf(n) + : toBigInteger(first).multiply(toBigInteger(second)); + } + case FLOAT: { + return Float.valueOf(first.floatValue() * second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() * second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.multiply(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + BigDecimal r = n1.multiply(n2); + return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r; + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number divide(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + if (n1 % n2 == 0) { + return Integer.valueOf(n1 / n2); + } + return Double.valueOf(((double) n1) / n2); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + if (n1 % n2 == 0) { + return Long.valueOf(n1 / n2); + } + return Double.valueOf(((double) n1) / n2); + } + case FLOAT: { + return Float.valueOf(first.floatValue() / second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() / second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + BigInteger[] divmod = n1.divideAndRemainder(n2); + if (divmod[1].equals(BigInteger.ZERO)) { + return divmod[0]; + } else { + BigDecimal bd1 = new BigDecimal(n1); + BigDecimal bd2 = new BigDecimal(n2); + return bd1.divide(bd2, minScale, roundingPolicy); + } + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + int scale1 = n1.scale(); + int scale2 = n2.scale(); + int scale = Math.max(scale1, scale2); + scale = Math.max(minScale, scale); + return n1.divide(n2, scale, roundingPolicy); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number modulus(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + return Integer.valueOf(first.intValue() % second.intValue()); + } + case LONG: { + return Long.valueOf(first.longValue() % second.longValue()); + } + case FLOAT: { + return Float.valueOf(first.floatValue() % second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() % second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.mod(n2); + } + case BIG_DECIMAL: { + throw new _MiscTemplateException("Can't calculate remainder on BigDecimals"); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new BugException(); + } + + @Override + public Number toNumber(String s) { + Number n = _NumberUtil.toBigDecimalOrDouble(s); + return n instanceof BigDecimal ? _NumberUtil.optimizeNumberRepresentation(n) : n; + } + + private static Map createClassCodesMap() { + Map map = new HashMap(17); + Integer intcode = Integer.valueOf(INTEGER); + map.put(Byte.class, intcode); + map.put(Short.class, intcode); + map.put(Integer.class, intcode); + map.put(Long.class, Integer.valueOf(LONG)); + map.put(Float.class, Integer.valueOf(FLOAT)); + map.put(Double.class, Integer.valueOf(DOUBLE)); + map.put(BigInteger.class, Integer.valueOf(BIG_INTEGER)); + map.put(BigDecimal.class, Integer.valueOf(BIG_DECIMAL)); + return map; + } + + private static int getClassCode(Number num) throws TemplateException { + try { + return ((Integer) classCodes.get(num.getClass())).intValue(); + } catch (NullPointerException e) { + if (num == null) { + throw new _MiscTemplateException("The Number object was null."); + } else { + throw new _MiscTemplateException("Unknown number type ", num.getClass().getName()); + } + } + } + + private static int getCommonClassCode(Number num1, Number num2) throws TemplateException { + int c1 = getClassCode(num1); + int c2 = getClassCode(num2); + int c = c1 > c2 ? c1 : c2; + // If BigInteger is combined with a Float or Double, the result is a + // BigDecimal instead of BigInteger in order not to lose the + // fractional parts. If Float is combined with Long, the result is a + // Double instead of Float to preserve the bigger bit width. + switch (c) { + case FLOAT: { + if ((c1 < c2 ? c1 : c2) == LONG) { + return DOUBLE; + } + break; + } + case BIG_INTEGER: { + int min = c1 < c2 ? c1 : c2; + if (min == DOUBLE || min == FLOAT) { + return BIG_DECIMAL; + } + break; + } + } + return c; + } + + private static BigInteger toBigInteger(Number num) { + return num instanceof BigInteger ? (BigInteger) num : new BigInteger(num.toString()); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html new file mode 100644 index 0000000..65688e2 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/impl/package.html @@ -0,0 +1,26 @@ +<!-- + 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. +--> +<html> +<head> +</head> +<body> +<p>Arithmetic used in templates: Standard implementations. This package is part of the +published API, that is, user code can safely depend on it.</p> +</body> +</html> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html new file mode 100644 index 0000000..62566ea --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/arithmetic/package.html @@ -0,0 +1,25 @@ +<!-- + 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. +--> +<html> +<head> +</head> +<body> +<p>Arithmetic used in templates: Base classes/interfaces.</p> +</body> +</html> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java new file mode 100644 index 0000000..363d4d8 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/debug/Breakpoint.java @@ -0,0 +1,83 @@ +/* + * 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.debug; + +import java.io.Serializable; + +/** + * Represents a breakpoint location consisting of a template name and a line number. + */ +public class Breakpoint implements Serializable, Comparable { + private static final long serialVersionUID = 1L; + + private final String templateName; + private final int line; + + /** + * Creates a new breakpoint. + * @param templateName the name of the template + * @param line the line number in the template where to put the breakpoint + */ + public Breakpoint(String templateName, int line) { + this.templateName = templateName; + this.line = line; + } + + /** + * Returns the line number of the breakpoint + */ + public int getLine() { + return line; + } + /** + * Returns the template name of the breakpoint + */ + public String getTemplateName() { + return templateName; + } + + @Override + public int hashCode() { + return templateName.hashCode() + 31 * line; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Breakpoint) { + Breakpoint b = (Breakpoint) o; + return b.templateName.equals(templateName) && b.line == line; + } + return false; + } + + @Override + public int compareTo(Object o) { + Breakpoint b = (Breakpoint) o; + int r = templateName.compareTo(b.templateName); + return r == 0 ? line - b.line : r; + } + + /** + * Returns the template name and the line number separated with a colon + */ + public String getLocationString() { + return templateName + ":" + line; + } +}
