http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java new file mode 100644 index 0000000..7ae5a71 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/APIModel.java @@ -0,0 +1,45 @@ +/* + * 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.model.impl; + +/** + * Exposes the Java API (and properties) of an object. + * + * <p> + * Notes: + * <ul> + * <li>The exposion level is inherited from the {@link DefaultObjectWrapper}</li> + * <li>But methods will always shadow properties and fields with identical name, regardless of {@link DefaultObjectWrapper} + * settings</li> + * </ul> + * + * @since 2.3.22 + */ +final class APIModel extends BeanModel { + + APIModel(Object object, DefaultObjectWrapper wrapper) { + super(object, wrapper, false); + } + + protected boolean isMethodsShadowItems() { + return true; + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java new file mode 100644 index 0000000..3b346e6 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ArgumentTypes.java @@ -0,0 +1,647 @@ +/* + * 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.model.impl; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._ClassUtil; + +/** + * The argument types of a method call; usable as cache key. + */ +final class ArgumentTypes { + + /** + * Conversion difficulty: Lowest; Java Reflection will do it automatically. + */ + private static final int CONVERSION_DIFFICULTY_REFLECTION = 0; + + /** + * Conversion difficulty: Medium: Java reflection API won't convert it, FreeMarker has to do it. + */ + private static final int CONVERSION_DIFFICULTY_FREEMARKER = 1; + + /** + * Conversion difficulty: Highest; conversion is not possible. + */ + private static final int CONVERSION_DIFFICULTY_IMPOSSIBLE = 2; + + /** + * The types of the arguments; for varags this contains the exploded list (not the array). + */ + private final Class<?>[] types; + + /** + * @param args The actual arguments. A varargs argument should be present exploded, no as an array. + */ + ArgumentTypes(Object[] args) { + int ln = args.length; + Class<?>[] typesTmp = new Class[ln]; + for (int i = 0; i < ln; ++i) { + Object arg = args[i]; + typesTmp[i] = arg == null + ? Null.class + : arg.getClass(); + } + + // `typesTmp` is used so the array is only modified before it's stored in the final `types` field (see JSR-133) + types = typesTmp; + } + + @Override + public int hashCode() { + int hash = 0; + for (Class<?> type : types) { + hash ^= type.hashCode(); + } + return hash; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ArgumentTypes) { + ArgumentTypes cs = (ArgumentTypes) o; + if (cs.types.length != types.length) { + return false; + } + for (int i = 0; i < types.length; ++i) { + if (cs.types[i] != types[i]) { + return false; + } + } + return true; + } + return false; + } + + /** + * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or + * {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}. + */ + MaybeEmptyCallableMemberDescriptor getMostSpecific( + List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) { + LinkedList<CallableMemberDescriptor> applicables = getApplicables(memberDescs, varArg); + if (applicables.isEmpty()) { + return EmptyCallableMemberDescriptor.NO_SUCH_METHOD; + } + if (applicables.size() == 1) { + return applicables.getFirst(); + } + + LinkedList<CallableMemberDescriptor> maximals = new LinkedList<>(); + for (CallableMemberDescriptor applicable : applicables) { + boolean lessSpecific = false; + for (Iterator<CallableMemberDescriptor> maximalsIter = maximals.iterator(); + maximalsIter.hasNext(); ) { + CallableMemberDescriptor maximal = maximalsIter.next(); + final int cmpRes = compareParameterListPreferability( + applicable.getParamTypes(), maximal.getParamTypes(), varArg); + if (cmpRes > 0) { + maximalsIter.remove(); + } else if (cmpRes < 0) { + lessSpecific = true; + } + } + if (!lessSpecific) { + maximals.addLast(applicable); + } + } + if (maximals.size() > 1) { + return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD; + } + return maximals.getFirst(); + } + + /** + * Tells if among the parameter list of two methods, which one fits this argument list better. + * This method assumes that the parameter lists are applicable to this argument lists; if that's not ensured, + * what the result will be is undefined. + * + * <p>The decision is made by comparing the preferability of each parameter types of the same position in a loop. + * At the end, the parameter list with the more preferred parameters will be the preferred one. If both parameter + * lists has the same amount of preferred parameters, the one that has the first (lower index) preferred parameter + * is the preferred one. Otherwise the two parameter list are considered to be equal in terms of preferability. + * + * <p>If there's no numerical conversion involved, the preferability of two parameter types is decided on how + * specific their types are. For example, {@code String} is more specific than {@link Object} (because + * {@code Object.class.isAssignableFrom(String.class)}-s), and so {@code String} is preferred. Primitive + * types are considered to be more specific than the corresponding boxing class (like {@code boolean} is more + * specific than {@code Boolean}, because the former can't store {@code null}). The preferability decision gets + * trickier when there's a possibility of numerical conversion from the actual argument type to the type of some of + * the parameters. If such conversion is only possible for one of the competing parameter types, that parameter + * automatically wins. If it's possible for both, {@link OverloadedNumberUtil#getArgumentConversionPrice} will + * be used to calculate the conversion "price", and the parameter type with lowest price wins. There are also + * a twist with array-to-list and list-to-array conversions; we try to avoid those, so the parameter where such + * conversion isn't needed will always win. + * + * @param paramTypes1 The parameter types of one of the competing methods + * @param paramTypes2 The parameter types of the other competing method + * @param varArg Whether these competing methods are varargs methods. + * @return More than 0 if the first parameter list is preferred, less then 0 if the other is preferred, + * 0 if there's no decision + */ + int compareParameterListPreferability(Class<?>[] paramTypes1, Class<?>[] paramTypes2, boolean varArg) { + final int argTypesLen = types.length; + final int paramTypes1Len = paramTypes1.length; + final int paramTypes2Len = paramTypes2.length; + //assert varArg || paramTypes1Len == paramTypes2Len; + + int paramList1WeakWinCnt = 0; + int paramList2WeakWinCnt = 0; + int paramList1WinCnt = 0; + int paramList2WinCnt = 0; + int paramList1StrongWinCnt = 0; + int paramList2StrongWinCnt = 0; + int paramList1VeryStrongWinCnt = 0; + int paramList2VeryStrongWinCnt = 0; + int firstWinerParamList = 0; + for (int i = 0; i < argTypesLen; i++) { + final Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg); + final Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg); + + final int winerParam; // 1 => paramType1; -1 => paramType2; 0 => draw + if (paramType1 == paramType2) { + winerParam = 0; + } else { + final Class<?> argType = types[i]; + final boolean argIsNum = Number.class.isAssignableFrom(argType); + + final int numConvPrice1; + if (argIsNum && _ClassUtil.isNumerical(paramType1)) { + final Class<?> nonPrimParamType1 = paramType1.isPrimitive() + ? _ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1; + numConvPrice1 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType1); + } else { + numConvPrice1 = Integer.MAX_VALUE; + } + // numConvPrice1 is Integer.MAX_VALUE if either: + // - argType and paramType1 aren't both numerical + // - FM doesn't know some of the numerical types, or the conversion between them is not allowed + + final int numConvPrice2; + if (argIsNum && _ClassUtil.isNumerical(paramType2)) { + final Class<?> nonPrimParamType2 = paramType2.isPrimitive() + ? _ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2; + numConvPrice2 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType2); + } else { + numConvPrice2 = Integer.MAX_VALUE; + } + + if (numConvPrice1 == Integer.MAX_VALUE) { + if (numConvPrice2 == Integer.MAX_VALUE) { // No numerical conversions anywhere + // List to array conversions (unwrapping sometimes makes a List instead of an array) + if (List.class.isAssignableFrom(argType) + && (paramType1.isArray() || paramType2.isArray())) { + if (paramType1.isArray()) { + if (paramType2.isArray()) { // both paramType1 and paramType2 are arrays + int r = compareParameterListPreferability_cmpTypeSpecificty( + paramType1.getComponentType(), paramType2.getComponentType()); + // Because we don't know if the List items are instances of the component + // type or not, we prefer the safer choice, which is the more generic array: + if (r > 0) { + winerParam = 2; + paramList2StrongWinCnt++; + } else if (r < 0) { + winerParam = 1; + paramList1StrongWinCnt++; + } else { + winerParam = 0; + } + } else { // paramType1 is array, paramType2 isn't + // Avoid List to array conversion if the other way makes any sense: + if (Collection.class.isAssignableFrom(paramType2)) { + winerParam = 2; + paramList2StrongWinCnt++; + } else { + winerParam = 1; + paramList1WeakWinCnt++; + } + } + } else { // paramType2 is array, paramType1 isn't + // Avoid List to array conversion if the other way makes any sense: + if (Collection.class.isAssignableFrom(paramType1)) { + winerParam = 1; + paramList1StrongWinCnt++; + } else { + winerParam = 2; + paramList2WeakWinCnt++; + } + } + } else if (argType.isArray() + && (List.class.isAssignableFrom(paramType1) + || List.class.isAssignableFrom(paramType2))) { + // Array to List conversions (unwrapping sometimes makes an array instead of a List) + if (List.class.isAssignableFrom(paramType1)) { + if (List.class.isAssignableFrom(paramType2)) { + // Both paramType1 and paramType2 extends List + winerParam = 0; + } else { + // Only paramType1 extends List + winerParam = 2; + paramList2VeryStrongWinCnt++; + } + } else { + // Only paramType2 extends List + winerParam = 1; + paramList1VeryStrongWinCnt++; + } + } else { // No list to/from array conversion + final int r = compareParameterListPreferability_cmpTypeSpecificty( + paramType1, paramType2); + if (r > 0) { + winerParam = 1; + if (r > 1) { + paramList1WinCnt++; + } else { + paramList1WeakWinCnt++; + } + } else if (r < 0) { + winerParam = -1; + if (r < -1) { + paramList2WinCnt++; + } else { + paramList2WeakWinCnt++; + } + } else { + winerParam = 0; + } + } + } else { // No num. conv. of param1, num. conv. of param2 + winerParam = -1; + paramList2WinCnt++; + } + } else if (numConvPrice2 == Integer.MAX_VALUE) { // Num. conv. of param1, not of param2 + winerParam = 1; + paramList1WinCnt++; + } else { // Num. conv. of both param1 and param2 + if (numConvPrice1 != numConvPrice2) { + if (numConvPrice1 < numConvPrice2) { + winerParam = 1; + if (numConvPrice1 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE + && numConvPrice2 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) { + paramList1StrongWinCnt++; + } else { + paramList1WinCnt++; + } + } else { + winerParam = -1; + if (numConvPrice2 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE + && numConvPrice1 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) { + paramList2StrongWinCnt++; + } else { + paramList2WinCnt++; + } + } + } else { + winerParam = (paramType1.isPrimitive() ? 1 : 0) - (paramType2.isPrimitive() ? 1 : 0); + if (winerParam == 1) paramList1WeakWinCnt++; + else if (winerParam == -1) paramList2WeakWinCnt++; + } + } + } // when paramType1 != paramType2 + + if (firstWinerParamList == 0 && winerParam != 0) { + firstWinerParamList = winerParam; + } + } // for each parameter types + + if (paramList1VeryStrongWinCnt != paramList2VeryStrongWinCnt) { + return paramList1VeryStrongWinCnt - paramList2VeryStrongWinCnt; + } else if (paramList1StrongWinCnt != paramList2StrongWinCnt) { + return paramList1StrongWinCnt - paramList2StrongWinCnt; + } else if (paramList1WinCnt != paramList2WinCnt) { + return paramList1WinCnt - paramList2WinCnt; + } else if (paramList1WeakWinCnt != paramList2WeakWinCnt) { + return paramList1WeakWinCnt - paramList2WeakWinCnt; + } else if (firstWinerParamList != 0) { // paramList1WinCnt == paramList2WinCnt + return firstWinerParamList; + } else { // still undecided + if (varArg) { + if (paramTypes1Len == paramTypes2Len) { + // If we had a 0-length varargs array in both methods, we also compare the types at the + // index of the varargs parameter, like if we had a single varargs argument. However, this + // time we don't have an argument type, so we can only decide based on type specificity: + if (argTypesLen == paramTypes1Len - 1) { + Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, argTypesLen, true); + Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, argTypesLen, true); + if (_ClassUtil.isNumerical(paramType1) && _ClassUtil.isNumerical(paramType2)) { + int r = OverloadedNumberUtil.compareNumberTypeSpecificity(paramType1, paramType2); + if (r != 0) return r; + // falls through + } + return compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2); + } else { + return 0; + } + } else { + // The method with more oms parameters wins: + return paramTypes1Len - paramTypes2Len; + } + } else { + return 0; + } + } + } + + /** + * Trivial comparison of type specificities; unaware of numerical conversions. + * + * @return Less-than-0, 0, or more-than-0 depending on which side is more specific. The absolute value is 1 if + * the difference is only in primitive VS non-primitive, more otherwise. + */ + private int compareParameterListPreferability_cmpTypeSpecificty( + final Class<?> paramType1, final Class<?> paramType2) { + // The more specific (smaller) type wins. + + final Class<?> nonPrimParamType1 = paramType1.isPrimitive() + ? _ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1; + final Class<?> nonPrimParamType2 = paramType2.isPrimitive() + ? _ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2; + + if (nonPrimParamType1 == nonPrimParamType2) { + if (nonPrimParamType1 != paramType1) { + if (nonPrimParamType2 != paramType2) { + return 0; // identical prim. types; shouldn't ever be reached + } else { + return 1; // param1 is prim., param2 is non prim. + } + } else if (nonPrimParamType2 != paramType2) { + return -1; // param1 is non-prim., param2 is prim. + } else { + return 0; // identical non-prim. types + } + } else if (nonPrimParamType2.isAssignableFrom(nonPrimParamType1)) { + return 2; + } else if (nonPrimParamType1.isAssignableFrom(nonPrimParamType2)) { + return -2; + } if (nonPrimParamType1 == Character.class && nonPrimParamType2.isAssignableFrom(String.class)) { + return 2; // A character is a 1 long string in FTL, so we pretend that it's a String subtype. + } if (nonPrimParamType2 == Character.class && nonPrimParamType1.isAssignableFrom(String.class)) { + return -2; + } else { + return 0; // unrelated types + } + } + + private static Class<?> getParamType(Class<?>[] paramTypes, int paramTypesLen, int i, boolean varArg) { + return varArg && i >= paramTypesLen - 1 + ? paramTypes[paramTypesLen - 1].getComponentType() + : paramTypes[i]; + } + + /** + * Returns all methods that are applicable to actual + * parameter types represented by this ArgumentTypes object. + */ + LinkedList<CallableMemberDescriptor> getApplicables( + List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) { + LinkedList<CallableMemberDescriptor> applicables = new LinkedList<>(); + for (ReflectionCallableMemberDescriptor memberDesc : memberDescs) { + int difficulty = isApplicable(memberDesc, varArg); + if (difficulty != CONVERSION_DIFFICULTY_IMPOSSIBLE) { + if (difficulty == CONVERSION_DIFFICULTY_REFLECTION) { + applicables.add(memberDesc); + } else if (difficulty == CONVERSION_DIFFICULTY_FREEMARKER) { + applicables.add(new SpecialConversionCallableMemberDescriptor(memberDesc)); + } else { + throw new BugException(); + } + } + } + return applicables; + } + + /** + * Returns if the supplied method is applicable to actual + * parameter types represented by this ArgumentTypes object, also tells + * how difficult that conversion is. + * + * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants. + */ + private int isApplicable(ReflectionCallableMemberDescriptor memberDesc, boolean varArg) { + final Class<?>[] paramTypes = memberDesc.getParamTypes(); + final int cl = types.length; + final int fl = paramTypes.length - (varArg ? 1 : 0); + if (varArg) { + if (cl < fl) { + return CONVERSION_DIFFICULTY_IMPOSSIBLE; + } + } else { + if (cl != fl) { + return CONVERSION_DIFFICULTY_IMPOSSIBLE; + } + } + + int maxDifficulty = 0; + for (int i = 0; i < fl; ++i) { + int difficulty = isMethodInvocationConvertible(paramTypes[i], types[i]); + if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) { + return CONVERSION_DIFFICULTY_IMPOSSIBLE; + } + if (maxDifficulty < difficulty) { + maxDifficulty = difficulty; + } + } + if (varArg) { + Class<?> varArgParamType = paramTypes[fl].getComponentType(); + for (int i = fl; i < cl; ++i) { + int difficulty = isMethodInvocationConvertible(varArgParamType, types[i]); + if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) { + return CONVERSION_DIFFICULTY_IMPOSSIBLE; + } + if (maxDifficulty < difficulty) { + maxDifficulty = difficulty; + } + } + } + return maxDifficulty; + } + + /** + * Determines whether a type is convertible to another type via + * method invocation conversion, and if so, what kind of conversion is needed. + * It treates the object type counterpart of primitive types as if they were the primitive types + * (that is, a Boolean actual parameter type matches boolean primitive formal type). This behavior + * is because this method is used to determine applicable methods for + * an actual parameter list, and primitive types are represented by + * their object duals in reflective method calls. + * @param formal the parameter type to which the actual + * parameter type should be convertible; possibly a primitive type + * @param actual the argument type; not a primitive type, maybe {@link Null}. + * + * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants. + */ + private int isMethodInvocationConvertible(final Class<?> formal, final Class<?> actual) { + // Check for identity or widening reference conversion + if (formal.isAssignableFrom(actual) && actual != CharacterOrString.class) { + return CONVERSION_DIFFICULTY_REFLECTION; + } else { + final Class<?> formalNP; + if (formal.isPrimitive()) { + if (actual == Null.class) { + return CONVERSION_DIFFICULTY_IMPOSSIBLE; + } + + formalNP = _ClassUtil.primitiveClassToBoxingClass(formal); + if (actual == formalNP) { + // Character and char, etc. + return CONVERSION_DIFFICULTY_REFLECTION; + } + } else { // formal is non-primitive + if (actual == Null.class) { + return CONVERSION_DIFFICULTY_REFLECTION; + } + + formalNP = formal; + } + if (Number.class.isAssignableFrom(actual) && Number.class.isAssignableFrom(formalNP)) { + return OverloadedNumberUtil.getArgumentConversionPrice(actual, formalNP) == Integer.MAX_VALUE + ? CONVERSION_DIFFICULTY_IMPOSSIBLE : CONVERSION_DIFFICULTY_REFLECTION; + } else if (formal.isArray()) { + // DefaultObjectWrapper method/constructor calls convert from List to array automatically + return List.class.isAssignableFrom(actual) + ? CONVERSION_DIFFICULTY_FREEMARKER : CONVERSION_DIFFICULTY_IMPOSSIBLE; + } else if (actual.isArray() && formal.isAssignableFrom(List.class)) { + // DefaultObjectWrapper method/constructor calls convert from array to List automatically + return CONVERSION_DIFFICULTY_FREEMARKER; + } else if (actual == CharacterOrString.class + && (formal.isAssignableFrom(String.class) + || formal.isAssignableFrom(Character.class) || formal == char.class)) { + return CONVERSION_DIFFICULTY_FREEMARKER; + } else { + return CONVERSION_DIFFICULTY_IMPOSSIBLE; + } + } + } + + /** + * Symbolizes the class of null (it's missing from Java). + */ + private static class Null { + + // Can't be instantiated + private Null() { } + + } + + /** + * Used instead of {@link ReflectionCallableMemberDescriptor} when the method is only applicable + * ({@link #isApplicable}) with conversion that Java reflection won't do. It delegates to a + * {@link ReflectionCallableMemberDescriptor}, but it adds the necessary conversions to the invocation methods. + */ + private static final class SpecialConversionCallableMemberDescriptor extends CallableMemberDescriptor { + + private final ReflectionCallableMemberDescriptor callableMemberDesc; + + SpecialConversionCallableMemberDescriptor(ReflectionCallableMemberDescriptor callableMemberDesc) { + this.callableMemberDesc = callableMemberDesc; + } + + @Override + TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args) throws TemplateModelException, + InvocationTargetException, IllegalAccessException { + convertArgsToReflectionCompatible(ow, args); + return callableMemberDesc.invokeMethod(ow, obj, args); + } + + @Override + Object invokeConstructor(DefaultObjectWrapper ow, Object[] args) throws IllegalArgumentException, + InstantiationException, IllegalAccessException, InvocationTargetException, TemplateModelException { + convertArgsToReflectionCompatible(ow, args); + return callableMemberDesc.invokeConstructor(ow, args); + } + + @Override + String getDeclaration() { + return callableMemberDesc.getDeclaration(); + } + + @Override + boolean isConstructor() { + return callableMemberDesc.isConstructor(); + } + + @Override + boolean isStatic() { + return callableMemberDesc.isStatic(); + } + + @Override + boolean isVarargs() { + return callableMemberDesc.isVarargs(); + } + + @Override + Class<?>[] getParamTypes() { + return callableMemberDesc.getParamTypes(); + } + + @Override + String getName() { + return callableMemberDesc.getName(); + } + + private void convertArgsToReflectionCompatible(DefaultObjectWrapper ow, Object[] args) throws TemplateModelException { + Class<?>[] paramTypes = callableMemberDesc.getParamTypes(); + int ln = paramTypes.length; + for (int i = 0; i < ln; i++) { + Class<?> paramType = paramTypes[i]; + final Object arg = args[i]; + if (arg == null) continue; + + // Handle conversion between List and array types, in both directions. Java reflection won't do such + // conversion, so we have to. + // Most reflection-incompatible conversions were already addressed by the unwrapping. The reason + // this one isn't is that for overloaded methods the hint of a given parameter position is often vague, + // so we may end up with a List even if some parameter types at that position are arrays (remember, we + // have to chose one unwrapping target type, despite that we have many possible overloaded methods), or + // the other way around (that happens when AdapterTemplateMoldel returns an array). + // Later, the overloaded method selection will assume that a List argument is applicable to an array + // parameter, and that an array argument is applicable to a List parameter, so we end up with this + // situation. + if (paramType.isArray() && arg instanceof List) { + args[i] = ow.listToArray((List<?>) arg, paramType, null); + } + if (arg.getClass().isArray() && paramType.isAssignableFrom(List.class)) { + args[i] = ow.arrayToList(arg); + } + + // Handle the conversion from CharacterOrString to Character or String: + if (arg instanceof CharacterOrString) { + if (paramType == Character.class || paramType == char.class + || (!paramType.isAssignableFrom(String.class) + && paramType.isAssignableFrom(Character.class))) { + args[i] = Character.valueOf(((CharacterOrString) arg).getAsChar()); + } else { + args[i] = ((CharacterOrString) arg).getAsString(); + } + } + } + } + + } + +} \ 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/model/impl/BeanAndStringModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java new file mode 100644 index 0000000..c154bba --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanAndStringModel.java @@ -0,0 +1,53 @@ +/* + * 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.model.impl; + +import org.apache.freemarker.core.model.TemplateScalarModel; + +/** + * Subclass of {@link BeanModel} that exposes the return value of the {@link + * java.lang.Object#toString()} method through the {@link TemplateScalarModel} + * interface. + */ +// [FM3] Treating all beans as FTL strings was certainly a bad idea in FM2. +public class BeanAndStringModel extends BeanModel implements TemplateScalarModel { + + /** + * Creates a new model that wraps the specified object with BeanModel + scalar + * functionality. + * @param object the object to wrap into a model. + * @param wrapper the {@link DefaultObjectWrapper} associated with this model. + * Every model has to have an associated {@link DefaultObjectWrapper} instance. The + * model gains many attributes from its wrapper, including the caching + * behavior, method exposure level, method-over-item shadowing policy etc. + */ + public BeanAndStringModel(Object object, DefaultObjectWrapper wrapper) { + super(object, wrapper); + } + + /** + * Returns the result of calling {@link Object#toString()} on the wrapped + * object. + */ + @Override + public String getAsString() { + return object.toString(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java new file mode 100644 index 0000000..91fe9dc --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java @@ -0,0 +1,339 @@ +/* + * 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.model.impl; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.freemarker.core._CoreLogs; +import org.apache.freemarker.core._DelayedFTLTypeDescription; +import org.apache.freemarker.core._DelayedJQuote; +import org.apache.freemarker.core._TemplateModelException; +import org.apache.freemarker.core.model.AdapterTemplateModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateHashModelEx; +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.TemplateModelWithAPISupport; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.WrapperTemplateModel; +import org.apache.freemarker.core.util._StringUtil; +import org.slf4j.Logger; + +/** + * A class that will wrap an arbitrary object into {@link org.apache.freemarker.core.model.TemplateHashModel} + * interface allowing calls to arbitrary property getters and invocation of + * accessible methods on the object from a template using the + * <tt>object.foo</tt> to access properties and <tt>object.bar(arg1, arg2)</tt> to + * invoke methods on it. You can also use the <tt>object.foo[index]</tt> syntax to + * access indexed properties. It uses Beans {@link java.beans.Introspector} + * to dynamically discover the properties and methods. + */ + +public class BeanModel + implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport { + + private static final Logger LOG = _CoreLogs.OBJECT_WRAPPER; + + protected final Object object; + protected final DefaultObjectWrapper wrapper; + + // We use this to represent an unknown value as opposed to known value of null (JR) + static final TemplateModel UNKNOWN = new SimpleScalar("UNKNOWN"); + + // I've tried to use a volatile ConcurrentHashMap field instead of HashMap + synchronized(this), but oddly it was + // a bit slower, at least on Java 8 u66. + private HashMap<Object, TemplateModel> memberCache; + + /** + * Creates a new model that wraps the specified object. Note that there are + * specialized subclasses of this class for wrapping arrays, collections, + * enumeration, iterators, and maps. Note also that the superclass can be + * used to wrap String objects if only scalar functionality is needed. You + * can also choose to delegate the choice over which model class is used for + * wrapping to {@link DefaultObjectWrapper#wrap(Object)}. + * @param object the object to wrap into a model. + * @param wrapper the {@link DefaultObjectWrapper} associated with this model. + * Every model has to have an associated {@link DefaultObjectWrapper} instance. The + * model gains many attributes from its wrapper, including the caching + * behavior, method exposure level, method-over-item shadowing policy etc. + */ + public BeanModel(Object object, DefaultObjectWrapper wrapper) { + // [2.4]: All models were introspected here, then the results was discareded, and get() will just do the + // introspection again. So is this necessary? (The inrospectNow parameter was added in 2.3.21 to allow + // lazy-introspecting DefaultObjectWrapper.trueModel|falseModel.) + this(object, wrapper, true); + } + + /** @since 2.3.21 */ + BeanModel(Object object, DefaultObjectWrapper wrapper, boolean inrospectNow) { + this.object = object; + this.wrapper = wrapper; + if (inrospectNow && object != null) { + // [2.4]: Could this be removed? + wrapper.getClassIntrospector().get(object.getClass()); + } + } + + /** + * Uses Beans introspection to locate a property or method with name + * matching the key name. If a method or property is found, it's wrapped + * into {@link org.apache.freemarker.core.model.TemplateMethodModelEx} (for a method or + * indexed property), or evaluated on-the-fly and the return value wrapped + * into appropriate model (for a simple property) Models for various + * properties and methods are cached on a per-class basis, so the costly + * introspection is performed only once per property or method of a class. + * (Side-note: this also implies that any class whose method has been called + * will be strongly referred to by the framework and will not become + * unloadable until this class has been unloaded first. Normally this is not + * an issue, but can be in a rare scenario where you invoke many classes on- + * the-fly. Also, as the cache grows with new classes and methods introduced + * to the framework, it may appear as if it were leaking memory. The + * framework does, however detect class reloads (if you happen to be in an + * environment that does this kind of things--servlet containers do it when + * they reload a web application) and flushes the cache. If no method or + * property matching the key is found, the framework will try to invoke + * methods with signature + * <tt>non-void-return-type get(java.lang.String)</tt>, + * then <tt>non-void-return-type get(java.lang.Object)</tt>, or + * alternatively (if the wrapped object is a resource bundle) + * <tt>Object get(java.lang.String)</tt>. + * @throws TemplateModelException if there was no property nor method nor + * a generic <tt>get</tt> method to invoke. + */ + @Override + public TemplateModel get(String key) + throws TemplateModelException { + Class<?> clazz = object.getClass(); + Map<Object, Object> classInfo = wrapper.getClassIntrospector().get(clazz); + TemplateModel retval = null; + + try { + Object fd = classInfo.get(key); + if (fd != null) { + retval = invokeThroughDescriptor(fd, classInfo); + } else { + retval = invokeGenericGet(classInfo, clazz, key); + } + if (retval == UNKNOWN) { + if (wrapper.isStrict()) { + throw new InvalidPropertyException("No such bean property: " + key); + } else { + logNoSuchKey(key, classInfo); + } + retval = wrapper.wrap(null); + } + return retval; + } catch (TemplateModelException e) { + throw e; + } catch (Exception e) { + throw new _TemplateModelException(e, + "An error has occurred when reading existing sub-variable ", new _DelayedJQuote(key), + "; see cause exception! The type of the containing value was: ", + new _DelayedFTLTypeDescription(this) + ); + } + } + + private void logNoSuchKey(String key, Map<?, ?> keyMap) { + if (LOG.isDebugEnabled()) { + LOG.debug("Key " + _StringUtil.jQuoteNoXSS(key) + " was not found on instance of " + + object.getClass().getName() + ". Introspection information for " + + "the class is: " + keyMap); + } + } + + /** + * Whether the model has a plain get(String) or get(Object) method + */ + + protected boolean hasPlainGetMethod() { + return wrapper.getClassIntrospector().get(object.getClass()).get(ClassIntrospector.GENERIC_GET_KEY) != null; + } + + private TemplateModel invokeThroughDescriptor(Object desc, Map<Object, Object> classInfo) + throws IllegalAccessException, InvocationTargetException, TemplateModelException { + // See if this particular instance has a cached implementation for the requested feature descriptor + TemplateModel cachedModel; + synchronized (this) { + cachedModel = memberCache != null ? memberCache.get(desc) : null; + } + + if (cachedModel != null) { + return cachedModel; + } + + TemplateModel resultModel = UNKNOWN; + if (desc instanceof PropertyDescriptor) { + PropertyDescriptor pd = (PropertyDescriptor) desc; + Method readMethod = pd.getReadMethod(); + if (readMethod != null) { + // Unlike in FreeMarker 2, we prefer the normal read method even if there's an indexed read method. + resultModel = wrapper.invokeMethod(object, readMethod, null); + // cachedModel remains null, as we don't cache these + } else if (desc instanceof IndexedPropertyDescriptor) { + // In FreeMarker 2 we have exposed such indexed properties as sequences, but they can't support + // the size() method, so we have discontinued that. People has to call the indexed read method like + // any other method. + resultModel = UNKNOWN; + } else { + throw new IllegalStateException("PropertyDescriptor.readMethod shouldn't be null"); + } + } else if (desc instanceof Field) { + resultModel = wrapper.wrap(((Field) desc).get(object)); + // cachedModel remains null, as we don't cache these + } else if (desc instanceof Method) { + Method method = (Method) desc; + resultModel = cachedModel = new JavaMethodModel( + object, method, ClassIntrospector.getArgTypes(classInfo, method), wrapper); + } else if (desc instanceof OverloadedMethods) { + resultModel = cachedModel = new OverloadedMethodsModel( + object, (OverloadedMethods) desc, wrapper); + } + + // If new cachedModel was created, cache it + if (cachedModel != null) { + synchronized (this) { + if (memberCache == null) { + memberCache = new HashMap<>(); + } + memberCache.put(desc, cachedModel); + } + } + return resultModel; + } + + void clearMemberCache() { + synchronized (this) { + memberCache = null; + } + } + + protected TemplateModel invokeGenericGet(Map/*<Object, Object>*/ classInfo, Class<?> clazz, String key) + throws IllegalAccessException, InvocationTargetException, + TemplateModelException { + Method genericGet = (Method) classInfo.get(ClassIntrospector.GENERIC_GET_KEY); + if (genericGet == null) { + return UNKNOWN; + } + + return wrapper.invokeMethod(object, genericGet, new Object[] { key }); + } + + protected TemplateModel wrap(Object obj) + throws TemplateModelException { + return wrapper.getOuterIdentity().wrap(obj); + } + + protected Object unwrap(TemplateModel model) + throws TemplateModelException { + return wrapper.unwrap(model); + } + + /** + * Tells whether the model is considered to be empty. + * It is empty if the wrapped object is a 0 length {@link String}, or an empty {@link Collection} or and empty + * {@link Map}, or an {@link Iterator} that has no more items, or a {@link Boolean#FALSE}, or {@code null}. + */ + @Override + public boolean isEmpty() { + if (object instanceof String) { + return ((String) object).length() == 0; + } + if (object instanceof Collection) { + return ((Collection<?>) object).isEmpty(); + } + if (object instanceof Iterator) { + return !((Iterator<?>) object).hasNext(); + } + if (object instanceof Map) { + return ((Map<?,?>) object).isEmpty(); + } + // [FM3] Why's FALSE empty? + return object == null || Boolean.FALSE.equals(object); + } + + /** + * Returns the same as {@link #getWrappedObject()}; to ensure that, this method will be final starting from 2.4. + * This behavior of {@link BeanModel} is assumed by some FreeMarker code. + */ + @Override + public Object getAdaptedObject(Class<?> hint) { + return object; // return getWrappedObject(); starting from 2.4 + } + + @Override + public Object getWrappedObject() { + return object; + } + + @Override + public int size() { + return wrapper.getClassIntrospector().keyCount(object.getClass()); + } + + @Override + public TemplateCollectionModel keys() { + return new CollectionAndSequence(new SimpleSequence(keySet(), wrapper)); + } + + @Override + public TemplateCollectionModel values() throws TemplateModelException { + List<Object> values = new ArrayList<>(size()); + TemplateModelIterator it = keys().iterator(); + while (it.hasNext()) { + String key = ((TemplateScalarModel) it.next()).getAsString(); + values.add(get(key)); + } + return new CollectionAndSequence(new SimpleSequence(values, wrapper)); + } + + @Override + public String toString() { + return object.toString(); + } + + /** + * Helper method to support TemplateHashModelEx. Returns the Set of + * Strings which are available via the TemplateHashModel + * interface. Subclasses that override <tt>invokeGenericGet</tt> to + * provide additional hash keys should also override this method. + */ + protected Set/*<Object>*/ keySet() { + return wrapper.getClassIntrospector().keySet(object.getClass()); + } + + @Override + public TemplateModel getAPI() throws TemplateModelException { + return wrapper.wrapAsAPI(object); + } + +} \ 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/model/impl/CallableMemberDescriptor.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java new file mode 100644 index 0000000..bbaf6bd --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CallableMemberDescriptor.java @@ -0,0 +1,56 @@ +/* + * 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.model.impl; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; + +/** + * Packs a {@link Method} or {@link Constructor} together with its parameter types. The actual + * {@link Method} or {@link Constructor} is not exposed by the API, because in rare cases calling them require + * type conversion that the Java reflection API can't do, hence the developer shouldn't be tempted to call them + * directly. + */ +abstract class CallableMemberDescriptor extends MaybeEmptyCallableMemberDescriptor { + + abstract TemplateModel invokeMethod(DefaultObjectWrapper ow, Object obj, Object[] args) + throws TemplateModelException, InvocationTargetException, IllegalAccessException; + + abstract Object invokeConstructor(DefaultObjectWrapper ow, Object[] args) + throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, + TemplateModelException; + + abstract String getDeclaration(); + + abstract boolean isConstructor(); + + abstract boolean isStatic(); + + abstract boolean isVarargs(); + + abstract Class[] getParamTypes(); + + abstract String getName(); + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java new file mode 100644 index 0000000..6026011 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/CharacterOrString.java @@ -0,0 +1,45 @@ +/* + * 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.model.impl; + +import org.apache.freemarker.core.model.TemplateScalarModel; + +/** + * Represents value unwrapped both to {@link Character} and {@link String}. This is needed for unwrapped overloaded + * method parameters where both {@link Character} and {@link String} occurs on the same parameter position when the + * {@link TemplateScalarModel} to unwrapp contains a {@link String} of length 1. + */ +final class CharacterOrString { + + private final String stringValue; + + CharacterOrString(String stringValue) { + this.stringValue = stringValue; + } + + String getAsString() { + return stringValue; + } + + char getAsChar() { + return stringValue.charAt(0); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java new file mode 100644 index 0000000..3fd3a2d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java @@ -0,0 +1,148 @@ +/* + * 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.model.impl; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.util._ClassUtil; + +/** + * Base class for hash models keyed by Java class names. + */ +abstract class ClassBasedModelFactory implements TemplateHashModel { + private final DefaultObjectWrapper wrapper; + + private final Map/*<String,TemplateModel>*/ cache = new ConcurrentHashMap(); + private final Set classIntrospectionsInProgress = new HashSet(); + + protected ClassBasedModelFactory(DefaultObjectWrapper wrapper) { + this.wrapper = wrapper; + } + + @Override + public TemplateModel get(String key) throws TemplateModelException { + try { + return getInternal(key); + } catch (Exception e) { + if (e instanceof TemplateModelException) { + throw (TemplateModelException) e; + } else { + throw new TemplateModelException(e); + } + } + } + + private TemplateModel getInternal(String key) throws TemplateModelException, ClassNotFoundException { + { + TemplateModel model = (TemplateModel) cache.get(key); + if (model != null) return model; + } + + final ClassIntrospector classIntrospector; + int classIntrospectorClearingCounter; + final Object sharedLock = wrapper.getSharedIntrospectionLock(); + synchronized (sharedLock) { + TemplateModel model = (TemplateModel) cache.get(key); + if (model != null) return model; + + while (model == null + && classIntrospectionsInProgress.contains(key)) { + // Another thread is already introspecting this class; + // waiting for its result. + try { + sharedLock.wait(); + model = (TemplateModel) cache.get(key); + } catch (InterruptedException e) { + throw new RuntimeException( + "Class inrospection data lookup aborded: " + e); + } + } + if (model != null) return model; + + // This will be the thread that introspects this class. + classIntrospectionsInProgress.add(key); + + // While the classIntrospector should not be changed from another thread, badly written apps can do that, + // and it's cheap to get the classIntrospector from inside the lock here: + classIntrospector = wrapper.getClassIntrospector(); + classIntrospectorClearingCounter = classIntrospector.getClearingCounter(); + } + try { + final Class clazz = _ClassUtil.forName(key); + + // This is called so that we trigger the + // class-reloading detector. If clazz is a reloaded class, + // the wrapper will in turn call our clearCache method. + // TODO: Why do we check it now and only now? + classIntrospector.get(clazz); + + TemplateModel model = createModel(clazz); + // Warning: model will be null if the class is not good for the subclass. + // For example, EnumModels#createModel returns null if clazz is not an enum. + + if (model != null) { + synchronized (sharedLock) { + // Save it into the cache, but only if nothing relevant has changed while we were outside the lock: + if (classIntrospector == wrapper.getClassIntrospector() + && classIntrospectorClearingCounter == classIntrospector.getClearingCounter()) { + cache.put(key, model); + } + } + } + return model; + } finally { + synchronized (sharedLock) { + classIntrospectionsInProgress.remove(key); + sharedLock.notifyAll(); + } + } + } + + void clearCache() { + synchronized (wrapper.getSharedIntrospectionLock()) { + cache.clear(); + } + } + + void removeFromCache(Class clazz) { + synchronized (wrapper.getSharedIntrospectionLock()) { + cache.remove(clazz.getName()); + } + } + + @Override + public boolean isEmpty() { + return false; + } + + protected abstract TemplateModel createModel(Class clazz) + throws TemplateModelException; + + protected DefaultObjectWrapper getWrapper() { + return wrapper; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java new file mode 100644 index 0000000..52321f0 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassChangeNotifier.java @@ -0,0 +1,32 @@ +/* + * 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.model.impl; + +/** + * Reports when the non-private interface of a class was changed to the subscribers. + */ +interface ClassChangeNotifier { + + /** + * @param classIntrospector Should only be weak-referenced from the monitor object. + */ + void subscribe(ClassIntrospector classIntrospector); + +}
