This is an automated email from the ASF dual-hosted git repository. ddekany pushed a commit to branch 2.3-gae in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit 1ea65e8e617f2fb00d70189cb7141e9518d69de3 Author: ddekany <[email protected]> AuthorDate: Wed Dec 18 23:10:40 2019 +0100 Added freemarker.ext.beans.MemberAccessPolicy interface, and the memberAccessPolicy property to BeansWrapper, and subclasses like DefaultObjectWrapper. This allows users to implement their own program logic to decide what members of classes will be exposed to the templates. The legacy "unsafe methods" mechanism also builds on the same now, and by setting a custom MemberAccessPolicy you completely replace that. --- .../java/freemarker/ext/beans/BeansWrapper.java | 6 +- .../ext/beans/BeansWrapperConfiguration.java | 10 +- .../freemarker/ext/beans/ClassIntrospector.java | 111 ++++-- .../ext/beans/ClassIntrospectorBuilder.java | 55 ++- .../ext/beans/ClassMemberAccessPolicy.java | 38 ++ ...Methods.java => DefaultMemberAccessPolicy.java} | 75 +++- .../freemarker/ext/beans/MemberAccessPolicy.java | 36 ++ .../java/freemarker/ext/beans/StaticModel.java | 4 +- .../java/freemarker/template/Configuration.java | 5 +- .../java/freemarker/template/_TemplateAPI.java | 1 + src/manual/en_US/book.xml | 10 + ...DefaultObjectWrapperMemberAccessPolicyTest.java | 409 +++++++++++++++++++++ 12 files changed, 687 insertions(+), 73 deletions(-) diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java index 953c2f4..d014a69 100644 --- a/src/main/java/freemarker/ext/beans/BeansWrapper.java +++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java @@ -96,7 +96,8 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { /** * At this level of exposure, all methods and properties of the - * wrapped objects are exposed to the template. + * wrapped objects are exposed to the template, and the {@link MemberAccessPolicy} + * will be ignored. */ public static final int EXPOSE_ALL = 0; @@ -858,9 +859,6 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { */ protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) { _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements); - if (incompatibleImprovements.intValue() < _TemplateAPI.VERSION_INT_2_3_0) { - throw new IllegalArgumentException("Version must be at least 2.3.0."); - } return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_27 ? Configuration.VERSION_2_3_27 : incompatibleImprovements.intValue() == _TemplateAPI.VERSION_INT_2_3_26 ? Configuration.VERSION_2_3_26 : is2324Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_24 diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java index 905bde9..f791d12 100644 --- a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java +++ b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java @@ -225,10 +225,18 @@ public abstract class BeansWrapperConfiguration implements Cloneable { classIntrospectorBuilder.setExposeFields(exposeFields); } + public MemberAccessPolicy getMemberAccessPolicy() { + return classIntrospectorBuilder.getMemberAccessPolicy(); + } + + public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) { + classIntrospectorBuilder.setMemberAccessPolicy(memberAccessPolicy); + } + public boolean getTreatDefaultMethodsAsBeanMembers() { return classIntrospectorBuilder.getTreatDefaultMethodsAsBeanMembers(); } - + /** See {@link BeansWrapper#setTreatDefaultMethodsAsBeanMembers(boolean)} */ public void setTreatDefaultMethodsAsBeanMembers(boolean treatDefaultMethodsAsBeanMembers) { classIntrospectorBuilder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers); diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java index c48a91b..72f26cb 100644 --- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java +++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java @@ -53,6 +53,7 @@ import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision; import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput; import freemarker.ext.util.ModelCache; import freemarker.log.Logger; +import freemarker.template.Version; import freemarker.template.utility.NullArgumentException; import freemarker.template.utility.SecurityUtilities; @@ -138,10 +139,11 @@ class ClassIntrospector { final int exposureLevel; final boolean exposeFields; + final MemberAccessPolicy memberAccessPolicy; final MethodAppearanceFineTuner methodAppearanceFineTuner; final MethodSorter methodSorter; final boolean treatDefaultMethodsAsBeanMembers; - final boolean bugfixed; + final Version incompatibleImprovements; /** See {@link #getHasSharedInstanceRestrictions()} */ final private boolean hasSharedInstanceRestrictions; @@ -178,10 +180,11 @@ class ClassIntrospector { this.exposureLevel = builder.getExposureLevel(); this.exposeFields = builder.getExposeFields(); + this.memberAccessPolicy = builder.getMemberAccessPolicy(); this.methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner(); this.methodSorter = builder.getMethodSorter(); this.treatDefaultMethodsAsBeanMembers = builder.getTreatDefaultMethodsAsBeanMembers(); - this.bugfixed = builder.isBugfixed(); + this.incompatibleImprovements = builder.getIncompatibleImprovements(); this.sharedLock = sharedLock; @@ -264,25 +267,26 @@ class ClassIntrospector { */ private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) { final Map<Object, Object> introspData = new HashMap<Object, Object>(); + ClassMemberAccessPolicy classMemberAccessPolicy = getClassMemberAccessPolicyIfNotIgnored(clazz); if (exposeFields) { - addFieldsToClassIntrospectionData(introspData, clazz); + addFieldsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy); } final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz); - addGenericGetToClassIntrospectionData(introspData, accessibleMethods); + addGenericGetToClassIntrospectionData(introspData, accessibleMethods, classMemberAccessPolicy); if (exposureLevel != BeansWrapper.EXPOSE_NOTHING) { try { - addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods); + addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods, classMemberAccessPolicy); } catch (IntrospectionException e) { LOG.warn("Couldn't properly perform introspection for class " + clazz, e); introspData.clear(); // FIXME NBC: Don't drop everything here. } } - addConstructorsToClassIntrospectionData(introspData, clazz); + addConstructorsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy); if (introspData.size() > 1) { return introspData; @@ -294,28 +298,30 @@ class ClassIntrospector { } } - private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz) - throws SecurityException { + private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz, + ClassMemberAccessPolicy classMemberAccessPolicy) throws SecurityException { Field[] fields = clazz.getFields(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; if ((field.getModifiers() & Modifier.STATIC) == 0) { - introspData.put(field.getName(), field); + if (classMemberAccessPolicy == null || classMemberAccessPolicy.isFieldExposed(field)) { + introspData.put(field.getName(), field); + } } } } private void addBeanInfoToClassIntrospectionData( - Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) - throws IntrospectionException { + Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods, + ClassMemberAccessPolicy classMemberAccessPolicy) throws IntrospectionException { BeanInfo beanInfo = Introspector.getBeanInfo(clazz); List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz); int pdasLength = pdas.size(); // Reverse order shouldn't mater, but we keep it to not risk backward incompatibility. for (int i = pdasLength - 1; i >= 0; --i) { addPropertyDescriptorToClassIntrospectionData( - introspData, pdas.get(i), clazz, - accessibleMethods); + introspData, pdas.get(i), + accessibleMethods, classMemberAccessPolicy); } if (exposureLevel < BeansWrapper.EXPOSE_PROPERTIES_ONLY) { @@ -327,7 +333,7 @@ class ClassIntrospector { IdentityHashMap<Method, Void> argTypesUsedByIndexerPropReaders = null; for (int i = mdsSize - 1; i >= 0; --i) { final Method method = getMatchingAccessibleMethod(mds.get(i).getMethod(), accessibleMethods); - if (method != null && isAllowedToExpose(method)) { + if (method != null && (isMethodExposed(classMemberAccessPolicy, method))) { decision.setDefaults(method); if (methodAppearanceFineTuner != null) { if (decisionInput == null) { @@ -344,7 +350,7 @@ class ClassIntrospector { (decision.getReplaceExistingProperty() || !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor))) { addPropertyDescriptorToClassIntrospectionData( - introspData, propDesc, clazz, accessibleMethods); + introspData, propDesc, accessibleMethods, classMemberAccessPolicy); } String methodKey = decision.getExposeMethodAs(); @@ -352,7 +358,8 @@ class ClassIntrospector { Object previous = introspData.get(methodKey); if (previous instanceof Method) { // Overloaded method - replace Method with a OverloadedMethods - OverloadedMethods overloadedMethods = new OverloadedMethods(bugfixed); + OverloadedMethods overloadedMethods = + new OverloadedMethods(is2321Bugfixed()); overloadedMethods.addMethod((Method) previous); overloadedMethods.addMethod(method); introspData.put(methodKey, overloadedMethods); @@ -652,9 +659,10 @@ class ClassIntrospector { } private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData, - PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) { + PropertyDescriptor pd, + Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) { Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods); - if (readMethod != null && !isAllowedToExpose(readMethod)) { + if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) { readMethod = null; } @@ -662,7 +670,7 @@ class ClassIntrospector { if (pd instanceof IndexedPropertyDescriptor) { indexedReadMethod = getMatchingAccessibleMethod( ((IndexedPropertyDescriptor) pd).getIndexedReadMethod(), accessibleMethods); - if (indexedReadMethod != null && !isAllowedToExpose(indexedReadMethod)) { + if (indexedReadMethod != null && !isMethodExposed(classMemberAccessPolicy, indexedReadMethod)) { indexedReadMethod = null; } if (indexedReadMethod != null) { @@ -679,31 +687,42 @@ class ClassIntrospector { } private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData, - Map<MethodSignature, List<Method>> accessibleMethods) { + Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) { Method genericGet = getFirstAccessibleMethod( MethodSignature.GET_STRING_SIGNATURE, accessibleMethods); if (genericGet == null) { genericGet = getFirstAccessibleMethod( MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods); } - if (genericGet != null) { + if (genericGet != null && isMethodExposed(classMemberAccessPolicy, genericGet)) { introspData.put(GENERIC_GET_KEY, genericGet); } } private void addConstructorsToClassIntrospectionData(final Map<Object, Object> introspData, - Class<?> clazz) { + Class<?> clazz, ClassMemberAccessPolicy classMemberAccessPolicy) { try { - Constructor<?>[] ctors = clazz.getConstructors(); - if (ctors.length == 1) { - Constructor<?> ctor = ctors[0]; - introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, ctor.getParameterTypes())); - } else if (ctors.length > 1) { - OverloadedMethods overloadedCtors = new OverloadedMethods(bugfixed); - for (int i = 0; i < ctors.length; i++) { - overloadedCtors.addConstructor(ctors[i]); + Constructor<?>[] ctorsUnfiltered = clazz.getConstructors(); + List<Constructor<?>> ctors = new ArrayList<Constructor<?>>(ctorsUnfiltered.length); + for (Constructor<?> ctor : ctorsUnfiltered) { + if (classMemberAccessPolicy == null || classMemberAccessPolicy.isConstructorExposed(ctor)) { + ctors.add(ctor); } - introspData.put(CONSTRUCTORS_KEY, overloadedCtors); + } + + if (!ctors.isEmpty()) { + final Object ctorsIntrospData; + if (ctors.size() == 1) { + Constructor<?> ctor = ctors.get(0); + ctorsIntrospData = new SimpleMethod(ctor, ctor.getParameterTypes()); + } else { + OverloadedMethods overloadedCtors = new OverloadedMethods(is2321Bugfixed()); + for (Constructor<?> ctor : ctors) { + overloadedCtors.addConstructor(ctor); + } + ctorsIntrospData = overloadedCtors; + } + introspData.put(CONSTRUCTORS_KEY, ctorsIntrospData); } } catch (SecurityException e) { LOG.warn("Can't discover constructors for class " + clazz.getName(), e); @@ -800,8 +819,28 @@ class ClassIntrospector { } } - boolean isAllowedToExpose(Method method) { - return exposureLevel < BeansWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method); + /** + * Returns the {@link ClassMemberAccessPolicy}, or {@code null} if it should be ignored because of other settings. + * (Ideally, all such rules should be contained in {@link ClassMemberAccessPolicy} alone, but that interface was + * added late in history.) + * + * @see #isMethodExposed(ClassMemberAccessPolicy, Method) + */ + ClassMemberAccessPolicy getClassMemberAccessPolicyIfNotIgnored(Class containingClass) { + return exposureLevel < BeansWrapper.EXPOSE_SAFE ? null : memberAccessPolicy.forClass(containingClass); + } + + /** + * @param classMemberAccessPolicyIfNotIgnored + * The value returned by {@link #getClassMemberAccessPolicyIfNotIgnored(Class)} + */ + static boolean isMethodExposed(ClassMemberAccessPolicy classMemberAccessPolicyIfNotIgnored, Method method) { + return classMemberAccessPolicyIfNotIgnored == null + || classMemberAccessPolicyIfNotIgnored.isMethodExposed(method); + } + + private boolean is2321Bugfixed() { + return BeansWrapper.is2321Bugfixed(incompatibleImprovements); } private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, Object> classInfo) { @@ -1035,7 +1074,11 @@ class ClassIntrospector { boolean getExposeFields() { return exposeFields; } - + + MemberAccessPolicy getMemberAccessPolicy() { + return memberAccessPolicy; + } + boolean getTreatDefaultMethodsAsBeanMembers() { return treatDefaultMethodsAsBeanMembers; } diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java index 1b54958..1f2d5e0 100644 --- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java +++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java @@ -26,21 +26,24 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import freemarker.template.Configuration; import freemarker.template.Version; import freemarker.template._TemplateAPI; +import freemarker.template.utility.NullArgumentException; final class ClassIntrospectorBuilder implements Cloneable { - - private final boolean bugfixed; private static final Map<ClassIntrospectorBuilder, Reference<ClassIntrospector>> INSTANCE_CACHE = new HashMap<ClassIntrospectorBuilder, Reference<ClassIntrospector>>(); private static final ReferenceQueue<ClassIntrospector> INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue<ClassIntrospector>(); - + + private final Version incompatibleImprovements; + // Properties and their *defaults*: private int exposureLevel = BeansWrapper.EXPOSE_SAFE; private boolean exposeFields; + private MemberAccessPolicy memberAccessPolicy; private boolean treatDefaultMethodsAsBeanMembers; private MethodAppearanceFineTuner methodAppearanceFineTuner; private MethodSorter methodSorter; @@ -51,23 +54,33 @@ final class ClassIntrospectorBuilder implements Cloneable { // - If you add a new field, review all methods in this class, also the ClassIntrospector constructor ClassIntrospectorBuilder(ClassIntrospector ci) { - bugfixed = ci.bugfixed; + incompatibleImprovements = ci.incompatibleImprovements; exposureLevel = ci.exposureLevel; exposeFields = ci.exposeFields; + memberAccessPolicy = ci.memberAccessPolicy; treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers; methodAppearanceFineTuner = ci.methodAppearanceFineTuner; - methodSorter = ci.methodSorter; + methodSorter = ci.methodSorter; } ClassIntrospectorBuilder(Version incompatibleImprovements) { // Warning: incompatibleImprovements must not affect this object at versions increments where there's no // change in the BeansWrapper.normalizeIncompatibleImprovements results. That is, this class may don't react - // to some version changes that affects BeansWrapper, but not the other way around. - bugfixed = BeansWrapper.is2321Bugfixed(incompatibleImprovements); + // to some version changes that affects BeansWrapper, but not the other way around. + this.incompatibleImprovements = normalizeIncompatibleImprovementsVersion(incompatibleImprovements); treatDefaultMethodsAsBeanMembers = incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_26; + memberAccessPolicy = DefaultMemberAccessPolicy.getInstance(this.incompatibleImprovements); } - + + private static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) { + _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements); + // All breakpoints here must occur in BeansWrapper.normalizeIncompatibleImprovements! + return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_30 ? Configuration.VERSION_2_3_30 + : incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_21 ? Configuration.VERSION_2_3_21 + : Configuration.VERSION_2_3_0; + } + @Override protected Object clone() { try { @@ -81,10 +94,11 @@ final class ClassIntrospectorBuilder implements Cloneable { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + (bugfixed ? 1231 : 1237); + result = prime * result + incompatibleImprovements.hashCode(); result = prime * result + (exposeFields ? 1231 : 1237); result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 1237); result = prime * result + exposureLevel; + result = prime * result + memberAccessPolicy.hashCode(); result = prime * result + System.identityHashCode(methodAppearanceFineTuner); result = prime * result + System.identityHashCode(methodSorter); return result; @@ -97,10 +111,11 @@ final class ClassIntrospectorBuilder implements Cloneable { if (getClass() != obj.getClass()) return false; ClassIntrospectorBuilder other = (ClassIntrospectorBuilder) obj; - if (bugfixed != other.bugfixed) return false; + if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false; if (exposeFields != other.exposeFields) return false; if (treatDefaultMethodsAsBeanMembers != other.treatDefaultMethodsAsBeanMembers) return false; if (exposureLevel != other.exposureLevel) return false; + if (!memberAccessPolicy.equals(other.memberAccessPolicy)) return false; if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false; if (methodSorter != other.methodSorter) return false; @@ -137,6 +152,15 @@ final class ClassIntrospectorBuilder implements Cloneable { this.treatDefaultMethodsAsBeanMembers = treatDefaultMethodsAsBeanMembers; } + public MemberAccessPolicy getMemberAccessPolicy() { + return memberAccessPolicy; + } + + public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) { + NullArgumentException.check(memberAccessPolicy); + this.memberAccessPolicy = memberAccessPolicy; + } + public MethodAppearanceFineTuner getMethodAppearanceFineTuner() { return methodAppearanceFineTuner; } @@ -153,6 +177,13 @@ final class ClassIntrospectorBuilder implements Cloneable { this.methodSorter = methodSorter; } + /** + * Returns the normalized incompatible improvements. + */ + public Version getIncompatibleImprovements() { + return incompatibleImprovements; + } + private static void removeClearedReferencesFromInstanceCache() { Reference<? extends ClassIntrospector> clearedRef; while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) { @@ -210,8 +241,4 @@ final class ClassIntrospectorBuilder implements Cloneable { } } - public boolean isBugfixed() { - return bugfixed; - } - } \ No newline at end of file diff --git a/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java new file mode 100644 index 0000000..3a1e0e6 --- /dev/null +++ b/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java @@ -0,0 +1,38 @@ +/* + * 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 freemarker.ext.beans; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Returned by {@link MemberAccessPolicy#forClass(Class)}. The idea is that {@link MemberAccessPolicy#forClass(Class)} + * is called once per class, and then the methods of the resulting {@link ClassMemberAccessPolicy} object will be + * called for each member of the class. This can speed up the process as the class-specific lookups will be done only + * once per class, not once per member. + * + * @since 2.3.30 + */ +public interface ClassMemberAccessPolicy { + boolean isMethodExposed(Method method); + boolean isConstructorExposed(Constructor<?> constructor); + boolean isFieldExposed(Field field); +} diff --git a/src/main/java/freemarker/ext/beans/UnsafeMethods.java b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java similarity index 56% rename from src/main/java/freemarker/ext/beans/UnsafeMethods.java rename to src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java index 249a6c1..8c1186d 100644 --- a/src/main/java/freemarker/ext/beans/UnsafeMethods.java +++ b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java @@ -19,6 +19,8 @@ package freemarker.ext.beans; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; @@ -27,24 +29,26 @@ import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; +import freemarker.template.Version; +import freemarker.template._TemplateAPI; import freemarker.template.utility.ClassUtil; -class UnsafeMethods { +/** + * Legacy black list based member access policy, used only to keep old behavior, as it can't provide meaningful safety. + * Do not use it if you allow untrusted users to edit templates! + * + * @since 2.3.30 + */ +public final class DefaultMemberAccessPolicy implements MemberAccessPolicy { private static final String UNSAFE_METHODS_PROPERTIES = "unsafeMethods.properties"; - private static final Set UNSAFE_METHODS = createUnsafeMethodsSet(); - - private UnsafeMethods() { } - - static boolean isUnsafeMethod(Method method) { - return UNSAFE_METHODS.contains(method); - } - - private static final Set createUnsafeMethodsSet() { + private static final Set<Method> UNSAFE_METHODS = createUnsafeMethodsSet(); + + private static Set<Method> createUnsafeMethodsSet() { try { Properties props = ClassUtil.loadProperties(BeansWrapper.class, UNSAFE_METHODS_PROPERTIES); - Set set = new HashSet(props.size() * 4 / 3, 1f); - Map primClasses = createPrimitiveClassesMap(); + Set<Method> set = new HashSet<Method>(props.size() * 4 / 3, 1f); + Map<String, Class<?>> primClasses = createPrimitiveClassesMap(); for (Object key : props.keySet()) { try { set.add(parseMethodSpec((String) key, primClasses)); @@ -64,20 +68,20 @@ class UnsafeMethods { } } - private static Method parseMethodSpec(String methodSpec, Map primClasses) + private static Method parseMethodSpec(String methodSpec, Map<String, Class<?>> primClasses) throws ClassNotFoundException, NoSuchMethodException { int brace = methodSpec.indexOf('('); int dot = methodSpec.lastIndexOf('.', brace); - Class clazz = ClassUtil.forName(methodSpec.substring(0, dot)); + Class<?> clazz = ClassUtil.forName(methodSpec.substring(0, dot)); String methodName = methodSpec.substring(dot + 1, brace); String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1); StringTokenizer tok = new StringTokenizer(argSpec, ","); int argcount = tok.countTokens(); - Class[] argTypes = new Class[argcount]; + Class<?>[] argTypes = new Class[argcount]; for (int i = 0; i < argcount; i++) { String argClassName = tok.nextToken(); - argTypes[i] = (Class) primClasses.get(argClassName); + argTypes[i] = primClasses.get(argClassName); if (argTypes[i] == null) { argTypes[i] = ClassUtil.forName(argClassName); } @@ -85,8 +89,8 @@ class UnsafeMethods { return clazz.getMethod(methodName, argTypes); } - private static Map createPrimitiveClassesMap() { - Map map = new HashMap(); + private static Map<String, Class<?>> createPrimitiveClassesMap() { + Map<String, Class<?>> map = new HashMap<String, Class<?>>(); map.put("boolean", Boolean.TYPE); map.put("byte", Byte.TYPE); map.put("char", Character.TYPE); @@ -98,4 +102,39 @@ class UnsafeMethods { return map; } + private static final DefaultMemberAccessPolicy INSTANCE = new DefaultMemberAccessPolicy(); + + private DefaultMemberAccessPolicy() { + } + + /** + * Returns the singleton that's compatible with the given incompatible improvements version. + */ + public static DefaultMemberAccessPolicy getInstance(Version incompatibleImprovements) { + _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements); + // All breakpoints here must occur in ClassIntrospectorBuilder.normalizeIncompatibleImprovementsVersion! + // Though currently we don't have any. + return INSTANCE; + } + + public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + return CLASS_MEMBER_ACCESS_POLICY_INSTANCE; + } + + private static final BacklistClassMemberAccessPolicy CLASS_MEMBER_ACCESS_POLICY_INSTANCE + = new BacklistClassMemberAccessPolicy(); + private static class BacklistClassMemberAccessPolicy implements ClassMemberAccessPolicy { + + public boolean isMethodExposed(Method method) { + return !UNSAFE_METHODS.contains(method); + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return true; + } + + public boolean isFieldExposed(Field field) { + return true; + } + } } diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java new file mode 100644 index 0000000..5d72fea --- /dev/null +++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.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 freemarker.ext.beans; + +/** + * Implement this to specify what class members are accessible from templates. Implementations must be thread + * safe, and instances should be generally singletons on JVM level. The last is because FreeMarker tries to cache + * class introspectors in a global (static, JVM-scope) cache for reuse, and that's only possible if the + * {@link MemberAccessPolicy} instances used at different places in the JVM are equal according to + * {@link #equals(Object) (and the singleton object of course {@link #equals(Object)} with itself). + * + * @since 2.3.30 + */ +public interface MemberAccessPolicy { + /** + * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class. + */ + ClassMemberAccessPolicy forClass(Class<?> containingClass); +} diff --git a/src/main/java/freemarker/ext/beans/StaticModel.java b/src/main/java/freemarker/ext/beans/StaticModel.java index 28c84bb..1b9e0f5 100644 --- a/src/main/java/freemarker/ext/beans/StaticModel.java +++ b/src/main/java/freemarker/ext/beans/StaticModel.java @@ -126,12 +126,14 @@ final class StaticModel implements TemplateHashModelEx { } } if (wrapper.getExposureLevel() < BeansWrapper.EXPOSE_PROPERTIES_ONLY) { + ClassMemberAccessPolicy classMemberAccessPolicy = + wrapper.getClassIntrospector().getClassMemberAccessPolicyIfNotIgnored(clazz); Method[] methods = clazz.getMethods(); for (int i = 0; i < methods.length; ++i) { Method method = methods[i]; int mod = method.getModifiers(); if (Modifier.isPublic(mod) && Modifier.isStatic(mod) - && wrapper.getClassIntrospector().isAllowedToExpose(method)) { + && ClassIntrospector.isMethodExposed(classMemberAccessPolicy, method)) { String name = method.getName(); Object obj = map.get(name); if (obj instanceof Method) { diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java index c169e9a..3f0031d 100644 --- a/src/main/java/freemarker/template/Configuration.java +++ b/src/main/java/freemarker/template/Configuration.java @@ -468,7 +468,10 @@ public class Configuration extends Configurable implements Cloneable, ParserConf /** FreeMarker version 2.3.29 (an {@link #Configuration(Version) incompatible improvements break-point}) */ public static final Version VERSION_2_3_29 = new Version(2, 3, 29); - + + /** FreeMarker version 2.3.30 (an {@link #Configuration(Version) incompatible improvements break-point}) */ + public static final Version VERSION_2_3_30 = new Version(2, 3, 30); + /** The default of {@link #getIncompatibleImprovements()}, currently {@link #VERSION_2_3_0}. */ public static final Version DEFAULT_INCOMPATIBLE_IMPROVEMENTS = Configuration.VERSION_2_3_0; /** @deprecated Use {@link #DEFAULT_INCOMPATIBLE_IMPROVEMENTS} instead. */ diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java index 30227ca..1b7bb0b 100644 --- a/src/main/java/freemarker/template/_TemplateAPI.java +++ b/src/main/java/freemarker/template/_TemplateAPI.java @@ -52,6 +52,7 @@ public class _TemplateAPI { public static final int VERSION_INT_2_3_27 = Configuration.VERSION_2_3_27.intValue(); public static final int VERSION_INT_2_3_28 = Configuration.VERSION_2_3_28.intValue(); public static final int VERSION_INT_2_3_29 = Configuration.VERSION_2_3_29.intValue(); + public static final int VERSION_INT_2_3_30 = Configuration.VERSION_2_3_30.intValue(); public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0); public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) { diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index c8a2aab..4b10a92 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -29187,6 +29187,16 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <itemizedlist> <listitem> <para>Added + <literal>freemarker.ext.beans.MemberAccessPolicy</literal> + interface, and the <literal>memberAccessPolicy</literal> + property to <literal>BeansWrapper</literal>, and subclasses like + <literal>DefaultObjectWrapper</literal>. This allows users to + implement their own program logic to decide what members of + classes will be exposed to the templates.</para> + </listitem> + + <listitem> + <para>Added <literal>Environment.getDataModelOrSharedVariable(String)</literal>.</para> </listitem> diff --git a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java new file mode 100644 index 0000000..355a769 --- /dev/null +++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java @@ -0,0 +1,409 @@ +/* + * 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 freemarker.ext.beans; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.ObjectWrapperAndUnwrapper; +import freemarker.template.SimpleNumber; +import freemarker.template.TemplateHashModel; +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; + +public class DefaultObjectWrapperMemberAccessPolicyTest { + + @Test + public void testMethodsWithDefaultMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper(); + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C()); + + assertNotNull(objM.get("m1")); + assertEquals("m2(true)", exec(ow, objM.get("m2"), true)); + assertEquals("staticM()", exec(ow, objM.get("staticM"))); + + assertEquals("x", getHashValue(ow, objM, "x")); + assertNotNull(objM.get("getX")); + assertNotNull(objM.get("setX")); + + assertNull(objM.get("notPublic")); + + assertNull(objM.get("notify")); + + // Because it was overridden, we allow it historically. + assertNotNull(objM.get("run")); + + assertEquals("safe wait(1)", exec(ow, objM.get("wait"), 1L)); + try { + exec(ow, objM.get("wait")); // 0 arg overload is not visible, a it's "unsafe" + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), containsString("wait(int)")); + } + } + + @Test + public void testFieldsWithDefaultMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper(); + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C()); + assertFieldsNotExposed(objM); + } + + private void assertFieldsNotExposed(TemplateHashModel objM) throws TemplateModelException { + assertNull(objM.get("publicField1")); + assertNull(objM.get("publicField2")); + assertNonPublicFieldsNotExposed(objM); + } + + private void assertNonPublicFieldsNotExposed(TemplateHashModel objM) throws TemplateModelException { + assertNull(objM.get("nonPublicField1")); + assertNull(objM.get("nonPublicField2")); + + // Strangely, static fields are banned historically, while static methods aren't. + assertNull(objM.get("STATIC_FIELD")); + } + + @Test + public void testGenericGetWithDefaultMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper(); + + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CWithGenericGet()); + + assertEquals("get(x)", getHashValue(ow, objM, "x")); + } + + @Test + public void testConstructorsWithDefaultMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapper ow = createDefaultMemberAccessPolicyObjectWrapper(); + assertNonPublicConstructorNotExposed(ow); + + assertEquals(CWithConstructor.class, ow.newInstance(CWithConstructor.class, Collections.emptyList()) + .getClass()); + + assertEquals(CWithOverloadedConstructor.class, + ow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList()) + .getClass()); + + assertEquals(CWithOverloadedConstructor.class, + ow.newInstance(CWithOverloadedConstructor.class, Collections.singletonList(new SimpleNumber(1))) + .getClass()); + } + + private void assertNonPublicConstructorNotExposed(DefaultObjectWrapper ow) { + try { + ow.newInstance(C.class, Collections.emptyList()); + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), containsString("constructor")); + } + } + + @Test + public void testExposeAllWithDefaultMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setExposureLevel(DefaultObjectWrapper.EXPOSE_ALL); + DefaultObjectWrapper ow = owb.build(); + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C()); + // Because the MemberAccessPolicy is ignored: + assertNotNull(objM.get("notify")); + assertFieldsNotExposed(objM); + } + + @Test + public void testExposeFieldsWithDefaultMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setExposeFields(true); + DefaultObjectWrapper ow = owb.build(); + { + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C()); + assertNull(objM.get("notify")); + assertEquals(1, getHashValue(ow, objM, "publicField1")); + assertEquals(2, getHashValue(ow, objM, "publicField2")); + assertNonPublicFieldsNotExposed(objM); + } + + { + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CExtended()); + assertNull(objM.get("notify")); + assertEquals(1, getHashValue(ow, objM, "publicField1")); + assertEquals(2, getHashValue(ow, objM, "publicField2")); + assertEquals(3, getHashValue(ow, objM, "publicField3")); + assertNonPublicFieldsNotExposed(objM); + } + } + + @Test + public void testMethodsWithCustomMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setMemberAccessPolicy(new MemberAccessPolicy() { + public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + String name = method.getName(); + Class<?>[] paramTypes = method.getParameterTypes(); + return name.equals("m3") + || (name.equals("m2") + && (paramTypes.length == 0 || paramTypes[0].equals(boolean.class))); + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return true; + } + + public boolean isFieldExposed(Field field) { + return true; + } + }; + } + }); + DefaultObjectWrapper ow = owb.build(); + + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C()); + assertNull(objM.get("m1")); + assertEquals("m3()", exec(ow, objM.get("m3"))); + assertEquals("m2()", exec(ow, objM.get("m2"))); + assertEquals("m2(true)", exec(ow, objM.get("m2"), true)); + try { + exec(ow, objM.get("m2"), 1); + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), containsString("overload")); + } + + assertNull(objM.get("notify")); + } + + @Test + public void testFieldsWithCustomMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setExposeFields(true); + owb.setMemberAccessPolicy(new MemberAccessPolicy() { + public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + return true; + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return true; + } + + public boolean isFieldExposed(Field field) { + return field.getName().equals("publicField1") + || field.getName().equals("nonPublicField1"); + } + }; + } + }); + DefaultObjectWrapper ow = owb.build(); + + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C()); + + assertNonPublicFieldsNotExposed(objM); + assertEquals(1, getHashValue(ow, objM, "publicField1")); + assertNull(getHashValue(ow, objM, "publicField2")); + } + + @Test + public void testGenericGetWithCustomMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setMemberAccessPolicy(new MemberAccessPolicy() { + public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + return false; + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return true; + } + + public boolean isFieldExposed(Field field) { + return true; + } + }; + } + }); + DefaultObjectWrapper ow = owb.build(); + + TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CWithGenericGet()); + assertNull(getHashValue(ow, objM, "x")); + } + + @Test + public void testConstructorsWithCustomMemberAccessPolicy() throws TemplateModelException { + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30); + owb.setMemberAccessPolicy(new MemberAccessPolicy() { + public ClassMemberAccessPolicy forClass(Class<?> containingClass) { + return new ClassMemberAccessPolicy() { + public boolean isMethodExposed(Method method) { + return true; + } + + public boolean isConstructorExposed(Constructor<?> constructor) { + return constructor.getDeclaringClass() == CWithOverloadedConstructor.class + && constructor.getParameterTypes().length == 1; + } + + public boolean isFieldExposed(Field field) { + return true; + } + }; + } + }); + DefaultObjectWrapper ow = owb.build(); + + assertNonPublicConstructorNotExposed(ow); + + try { + assertEquals(CWithConstructor.class, + ow.newInstance(CWithConstructor.class, Collections.emptyList()).getClass()); + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), containsString("constructor")); + } + + try { + ow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList()); + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), containsString("constructor")); + } + + assertEquals(CWithOverloadedConstructor.class, + ow.newInstance(CWithOverloadedConstructor.class, + Collections.singletonList(new SimpleNumber(1))).getClass()); + } + + private static DefaultObjectWrapper createDefaultMemberAccessPolicyObjectWrapper() { + return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build(); + } + + private static Object getHashValue(ObjectWrapperAndUnwrapper ow, TemplateHashModel objM, String key) + throws TemplateModelException { + return ow.unwrap(objM.get(key)); + } + + private static Object exec(ObjectWrapperAndUnwrapper ow, TemplateModel objM, Object... args) throws TemplateModelException { + assertThat(objM, instanceOf(TemplateMethodModelEx.class)); + List<TemplateModel> argModels = new ArrayList<TemplateModel>(); + for (Object arg : args) { + argModels.add(ow.wrap(arg)); + } + Object returnValue = ((TemplateMethodModelEx) objM).exec(argModels); + return unwrap(ow, returnValue); + } + + private static Object unwrap(ObjectWrapperAndUnwrapper ow, Object returnValue) throws TemplateModelException { + return returnValue instanceof TemplateModel ? ow.unwrap((TemplateModel) returnValue) : returnValue; + } + + public static class C extends Thread { + public static final int STATIC_FIELD = 1; + public int publicField1 = 1; + public int publicField2 = 2; + protected int nonPublicField1 = 1; + private int nonPublicField2 = 2; + + // Non-public + C() { + + } + + void notPublic() { + } + + public void m1() { + } + + public String m2() { + return "m2()"; + } + + public String m2(int otherOverload) { + return "m2(" + otherOverload + ")"; + } + + public String m2(boolean otherOverload) { + return "m2(" + otherOverload + ")"; + } + + public String m3() { + return "m3()"; + } + + public static String staticM() { + return "staticM()"; + } + + public String getX() { + return "x"; + } + + public void setX(String x) { + } + + public String wait(int otherOverload) { + return "safe wait(" + otherOverload + ")"; + } + + @Override + public void run() { + return; + } + } + + public static class CExtended extends C { + public int publicField3 = 3; + } + + public static class CWithGenericGet extends Thread { + public String get(String key) { + return "get(" + key + ")"; + } + } + + public static class CWithConstructor implements TemplateModel { + public CWithConstructor() { + } + } + + public static class CWithOverloadedConstructor implements TemplateModel { + public CWithOverloadedConstructor() { + } + + public CWithOverloadedConstructor(int x) { + } + } + +}
