Added WhitelistMemberAccessPolicy and related internal classes
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index d014a69..d67c3ac 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -658,6 +658,29 @@
}
}
+ /**
+ * @since 2.3.30
+ */
+ public MemberAccessPolicy getMemberAccessPolicy() {
+ return classIntrospector.getMemberAccessPolicy();
+ }
+
+ /**
+ * Used to customize what members will be hidden;
+ * see {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} for more.
+ *
+ * @since 2.3.30
+ */
+ public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+ checkModifiable();
+
+ if (classIntrospector.getMemberAccessPolicy() != memberAccessPolicy) {
+ ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
+ builder.setMemberAccessPolicy(memberAccessPolicy);
+ replaceClassIntrospector(builder);
+ }
+ }
+
MethodSorter getMethodSorter() {
return classIntrospector.getMethodSorter();
}
@@ -1567,7 +1590,7 @@
Object ctors = classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY);
if (ctors == null) {
throw new TemplateModelException("Class " + clazz.getName() +
- " has no public constructors.");
+ " has no exposed constructors.");
}
Constructor<?> ctor = null;
Object[] objargs;
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index 72f26cb..630bf95 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -78,6 +78,11 @@
private static final String JREBEL_INTEGRATION_ERROR_MSG
= "Error initializing JRebel integration. JRebel integration disabled.";
+ private static final ExecutableMemberSignature GET_STRING_SIGNATURE =
+ new ExecutableMemberSignature("get", new Class[] { String.class });
+ private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE =
+ new ExecutableMemberSignature("get", new Class[] { Object.class });
+
/**
* When this property is true, some things are stricter. This is mostly to catch suspicious things in development
* that can otherwise be valid situations.
@@ -205,7 +210,7 @@
return new ClassIntrospectorBuilder(this);
}
- // ------------------------------------------------------------------------------------------------------------------
+ // -----------------------------------------------------------------------------------------------------------------
// Introspection:
/**
@@ -273,7 +278,7 @@
addFieldsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy);
}
- final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
+ final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
addGenericGetToClassIntrospectionData(introspData, accessibleMethods, classMemberAccessPolicy);
@@ -312,7 +317,8 @@
}
private void addBeanInfoToClassIntrospectionData(
- Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods,
+ Map<Object, Object> introspData, Class<?> clazz,
+ Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
ClassMemberAccessPolicy classMemberAccessPolicy) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz);
@@ -660,7 +666,8 @@
private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
PropertyDescriptor pd,
- Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) {
+ Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+ ClassMemberAccessPolicy classMemberAccessPolicy) {
Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods);
if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) {
readMethod = null;
@@ -687,12 +694,11 @@
}
private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
- Map<MethodSignature, List<Method>> accessibleMethods, ClassMemberAccessPolicy classMemberAccessPolicy) {
- Method genericGet = getFirstAccessibleMethod(
- MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+ Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+ ClassMemberAccessPolicy classMemberAccessPolicy) {
+ Method genericGet = getFirstAccessibleMethod(GET_STRING_SIGNATURE, accessibleMethods);
if (genericGet == null) {
- genericGet = getFirstAccessibleMethod(
- MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+ genericGet = getFirstAccessibleMethod(GET_OBJECT_SIGNATURE, accessibleMethods);
}
if (genericGet != null && isMethodExposed(classMemberAccessPolicy, genericGet)) {
introspData.put(GENERIC_GET_KEY, genericGet);
@@ -730,23 +736,24 @@
}
/**
- * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the
- * class is not public, retrieves methods with same signature as its public methods from public superclasses and
- * interfaces. Basically upcasts every method to the nearest accessible method.
+ * Retrieves mapping of {@link ExecutableMemberSignature}-s to a {@link List} of accessible methods for a class. In
+ * case the class is not public, retrieves methods with same signature as its public methods from public
+ * superclasses and interfaces. Basically upcasts every method to the nearest accessible method.
*/
- private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
- Map<MethodSignature, List<Method>> accessibles = new HashMap<MethodSignature, List<Method>>();
+ private static Map<ExecutableMemberSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
+ Map<ExecutableMemberSignature, List<Method>> accessibles = new HashMap<ExecutableMemberSignature, List<Method>>();
discoverAccessibleMethods(clazz, accessibles);
return accessibles;
}
- private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) {
+ private static void discoverAccessibleMethods(
+ Class<?> clazz, Map<ExecutableMemberSignature, List<Method>> accessibles) {
if (Modifier.isPublic(clazz.getModifiers())) {
try {
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
- MethodSignature sig = new MethodSignature(method);
+ ExecutableMemberSignature sig = new ExecutableMemberSignature(method);
// Contrary to intuition, a class can actually have several
// different methods with same signature *but* different
// return types. These can't be constructed using Java the
@@ -785,11 +792,11 @@
}
}
- private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) {
+ private static Method getMatchingAccessibleMethod(Method m, Map<ExecutableMemberSignature, List<Method>> accessibles) {
if (m == null) {
return null;
}
- MethodSignature sig = new MethodSignature(m);
+ ExecutableMemberSignature sig = new ExecutableMemberSignature(m);
List<Method> ams = accessibles.get(sig);
if (ams == null) {
return null;
@@ -802,7 +809,8 @@
return null;
}
- private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) {
+ private static Method getFirstAccessibleMethod(
+ ExecutableMemberSignature sig, Map<ExecutableMemberSignature, List<Method>> accessibles) {
List<Method> ams = accessibles.get(sig);
if (ams == null || ams.isEmpty()) {
return null;
@@ -853,39 +861,6 @@
return argTypes;
}
- private static final class MethodSignature {
- private static final MethodSignature GET_STRING_SIGNATURE =
- new MethodSignature("get", new Class[] { String.class });
- private static final MethodSignature GET_OBJECT_SIGNATURE =
- new MethodSignature("get", new Class[] { Object.class });
-
- private final String name;
- private final Class<?>[] args;
-
- private MethodSignature(String name, Class<?>[] args) {
- this.name = name;
- this.args = args;
- }
-
- MethodSignature(Method method) {
- this(method.getName(), method.getParameterTypes());
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof MethodSignature) {
- MethodSignature ms = (MethodSignature) o;
- return ms.name.equals(name) && Arrays.equals(args, ms.args);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem?
- }
- }
-
// -----------------------------------------------------------------------------------------------------------------
// Cache management:
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index 1f2d5e0..e2847ab 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -156,6 +156,12 @@
return memberAccessPolicy;
}
+ /**
+ * Sets the {@link MemberAccessPolicy}; default is {@link DefaultMemberAccessPolicy#getInstance(Version)}, which
+ * is not appropriate if template editors aren't trusted.
+ *
+ * @since 2.3.30
+ */
public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
NullArgumentException.check(memberAccessPolicy);
this.memberAccessPolicy = memberAccessPolicy;
diff --git a/src/main/java/freemarker/ext/beans/ConstructorMatcher.java b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
new file mode 100644
index 0000000..2c94576
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * {@link MemberMatcher} for constructors.
+ *
+ * @since 2.3.30
+ */
+final class ConstructorMatcher extends MemberMatcher<Constructor<?>, ExecutableMemberSignature> {
+ @Override
+ protected ExecutableMemberSignature toMemberSignature(Constructor<?> member) {
+ return new ExecutableMemberSignature(member);
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java
new file mode 100644
index 0000000..dfba692
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Used as a key in a {@link Map} or {@link Set} of methods or constructors.
+ *
+ * @since 2.3.30
+ */
+final class ExecutableMemberSignature {
+ private final String name;
+ private final Class<?>[] args;
+
+ ExecutableMemberSignature(String name, Class<?>[] args) {
+ this.name = name;
+ this.args = args;
+ }
+
+ /**
+ * Uses the method name, and the parameter types.
+ */
+ ExecutableMemberSignature(Method method) {
+ this(method.getName(), method.getParameterTypes());
+ }
+
+ /**
+ * Doesn't use the constructor name, only the parameter types.
+ */
+ ExecutableMemberSignature(Constructor<?> constructor) {
+ this("<init>", constructor.getParameterTypes());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ExecutableMemberSignature) {
+ ExecutableMemberSignature ms = (ExecutableMemberSignature) o;
+ return ms.name.equals(name) && Arrays.equals(args, ms.args);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() + args.length * 31;
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/FieldMatcher.java b/src/main/java/freemarker/ext/beans/FieldMatcher.java
new file mode 100644
index 0000000..f67bf14
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/FieldMatcher.java
@@ -0,0 +1,34 @@
+/*
+ * 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.Field;
+
+/**
+ * {@link MemberMatcher} for fields.
+ *
+ * @since 2.3.30
+ */
+final class FieldMatcher extends MemberMatcher<Field, String> {
+ @Override
+ protected String toMemberSignature(Field member) {
+ return member.getName();
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
index 5d72fea..c8de56d 100644
--- a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
+++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
@@ -19,18 +19,45 @@
package freemarker.ext.beans;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateModel;
+
/**
- * 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).
+ * Implement this to specify what class members are accessible from templates.
+ *
+ * <p>The instance is usually set via {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} (or if
+ * you use {@link DefaultObjectWrapper}, with
+ * {@link DefaultObjectWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)}).
+ *
+ * <p>As {@link BeansWrapper}, and its subclasses like {@link DefaultObjectWrapper}, only discover public
+ * members, it's pointless to whitelist non-public members. An {@link MemberAccessPolicy} is a filter applied to
+ * the set of members that {@link BeansWrapper} intends to expose on the first place. (Also, while public members
+ * declared in non-public classes are discovered by {@link BeansWrapper}, Java reflection will not allow accessing those
+ * normally, so generally it's not useful to whitelist those either.)
+ *
+ * <p>Note that if you add {@link TemplateModel}-s directly to the data-model, those are not wrapped by the
+ * {@link ObjectWrapper}, and so the {@link MemberAccessPolicy} won't affect those.
+ *
+ * <p>Implementations must be thread-safe, and instances generally should be singletons on JVM level. FreeMarker
+ * caches its class metadata in a global (static, JVM-scope) cache for shared use, and the {@link MemberAccessPolicy}
+ * used is part of the cache key. Thus {@link MemberAccessPolicy} instances used at different places in the JVM
+ * should be equal according to {@link Object#equals(Object)}, as far as they implement exactly the same policy. It's
+ * not recommended to override {@link Object#equals(Object)}; use singletons and the default
+ * {@link Object#equals(Object)} implementation if possible.
*
* @since 2.3.30
*/
public interface MemberAccessPolicy {
/**
* Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class.
+ * {@link ClassMemberAccessPolicy} implementations need not be thread-safe. Because class introspection results are
+ * cached, and so this method is usually only called once for a given class, the {@link ClassMemberAccessPolicy}
+ * instances shouldn't be cached by the implementation of this method.
+ *
+ * @param contextClass
+ * The exact class of object from which members will be get in the templates.
*/
- ClassMemberAccessPolicy forClass(Class<?> containingClass);
+ ClassMemberAccessPolicy forClass(Class<?> contextClass);
}
diff --git a/src/main/java/freemarker/ext/beans/MemberMatcher.java b/src/main/java/freemarker/ext/beans/MemberMatcher.java
new file mode 100644
index 0000000..2b52178
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberMatcher.java
@@ -0,0 +1,111 @@
+/*
+ * 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.Member;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * For implementing a whitelist or blacklist of class members in {@link MemberAccessPolicy} implementations.
+ * A {@link MemberMatcher} filters by name and/or signature, but not by by visibility, as
+ * the visibility condition is orthogonal to the whitelist or blacklist content.
+ *
+ * @since 2.3.30
+ */
+abstract class MemberMatcher<M extends Member, S> {
+ private final Map<S, Types> signaturesToUpperBoundTypes = new HashMap<S, Types>();
+
+ private static class Types {
+ private final Set<Class<?>> set = new HashSet<Class<?>>();
+ private boolean containsInterfaces;
+ }
+
+ /**
+ * Returns the {@link Map} lookup key used to match the member.
+ */
+ protected abstract S toMemberSignature(M member);
+
+ /**
+ * Adds a member that this {@link MemberMatcher} will match.
+ *
+ * @param upperBoundType
+ * The type of the actual object that contains the member must {@code instanceof} this.
+ * @param member
+ * The member that should match (when the upper bound class condition is also fulfilled). Only the name
+ * and/or signature of the member will be used for the condition, not the actual member object.
+ */
+ void addMatching(Class<?> upperBoundType, M member) {
+ Class<?> declaringClass = member.getDeclaringClass();
+ if (!declaringClass.isAssignableFrom(upperBoundType)) {
+ throw new IllegalArgumentException("Upper bound class " + upperBoundType.getName() + " is not the same "
+ + "type or a subtype of the declaring type of member " + member + ".");
+ }
+
+ S memberSignature = toMemberSignature(member);
+ Types upperBoundTypes = signaturesToUpperBoundTypes.get(memberSignature);
+ if (upperBoundTypes == null) {
+ upperBoundTypes = new Types();
+ signaturesToUpperBoundTypes.put(memberSignature, upperBoundTypes);
+ }
+ upperBoundTypes.set.add(upperBoundType);
+ if (upperBoundType.isInterface()) {
+ upperBoundTypes.containsInterfaces = true;
+ }
+ }
+
+ /**
+ * Returns if the given member, if it's referred through the given class, is matched by this {@link MemberMatcher}.
+ *
+ * @param contextClass The actual class through which we access the member
+ * @param member The member that we intend to access
+ *
+ * @return If there was match in this {@link MemberMatcher}.
+ */
+ boolean matches(Class<?> contextClass, M member) {
+ S memberSignature = toMemberSignature(member);
+ Types upperBoundTypes = signaturesToUpperBoundTypes.get(memberSignature);
+
+ return upperBoundTypes != null && containsTypeOrSuperType(upperBoundTypes, contextClass);
+ }
+
+ private static boolean containsTypeOrSuperType(Types types, Class<?> c) {
+ if (c == null) {
+ return false;
+ }
+
+ if (types.set.contains(c)) {
+ return true;
+ }
+ if (containsTypeOrSuperType(types, c.getSuperclass())) {
+ return true;
+ }
+ if (types.containsInterfaces) {
+ for (Class<?> anInterface : c.getInterfaces()) {
+ if (containsTypeOrSuperType(types, anInterface)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/MethodMatcher.java b/src/main/java/freemarker/ext/beans/MethodMatcher.java
new file mode 100644
index 0000000..7df56be
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MethodMatcher.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.Method;
+
+/**
+ * {@link MemberMatcher} for methods.
+ *
+ * <p>The return type (and visibility) of the methods will be ignored, only the method name and its parameter types
+ * matter. (The {@link MemberAccessPolicy}, and even {@link BeansWrapper} itself will still filter by visibility, it's
+ * just not the duty of the {@link MemberMatcher}.)
+ *
+ * @since 2.3.30
+ */
+final class MethodMatcher extends MemberMatcher<Method, ExecutableMemberSignature> {
+ @Override
+ protected ExecutableMemberSignature toMemberSignature(Method member) {
+ return new ExecutableMemberSignature(member);
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/TemplateAccessible.java b/src/main/java/freemarker/ext/beans/TemplateAccessible.java
new file mode 100644
index 0000000..5e07873
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/TemplateAccessible.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 freemarker.ext.beans;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.ObjectWrapper;
+
+/**
+ * Indicates that the the annotated member can be exposed to templates; if the annotated member will be actually
+ * exposed depends on the {@link ObjectWrapper} in use, and how that was configured. When used with
+ * {@link BeansWrapper} or its subclasses, most notably with {@link DefaultObjectWrapper}, and you also set the
+ * {@link MemberAccessPolicy} to a {@link WhitelistMemberAccessPolicy}, it will acts as if the members annotated with
+ * this are in the whitelist. Note that adding something to the whitelist doesn't necessary make it visible from
+ * templates; see {@link WhitelistMemberAccessPolicy} documentation.
+ *
+ * @since 2.3.30
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+public @interface TemplateAccessible {
+}
diff --git a/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
new file mode 100644
index 0000000..5e8945a
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
@@ -0,0 +1,411 @@
+/*
+ * 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;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import freemarker.log.Logger;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * Whitelist-based member access policy, that is, only members that you have explicitly whitelisted will be accessible.
+ * The whitelist content is application specific, and can be significant work to put together, but it's the only way
+ * you can achieve any practical safety if you don't fully trust the users who can edit templates. Of course, this only
+ * can deal with the {@link ObjectWrapper} aspect of safety; please check the Manual to see what else is needed. Also,
+ * since this is related to security, read the documentation of {@link MemberAccessPolicy}, to know about the
+ * pitfalls and edge cases related to {@link MemberAccessPolicy}-es in general.
+ *
+ * <p>There are two ways you can add members to the whitelist:
+ * <ul>
+ * <li>Via a list of member selectors passed to the constructor
+ * <li>Via {@link TemplateAccessible} annotation
+ * </ul>
+ *
+ * <p>When a member is whitelisted, it's identified by the following data (with the example of
+ * {@code com.example.MyClass.myMethod(int, int)} being whitelisted):
+ * <ul>
+ * <li>Upper bound class ({@code com.example.MyClass} in the example)
+ * <li>Member name ({@code myMethod} in the example), except for constructors where it's unused
+ * <li>Parameter types ({@code int, int} in the example), except for fields where it's unused
+ * </ul>
+ *
+ * <p>Once you have whitelisted a member in the upper bound class, it will be automatically whitelisted in all
+ * subclasses of that, even if the whitelisted member is a field or constructor (which doesn't support overriding, but
+ * it will be treated as such if the field name or constructor parameter types match).
+ * It's called "upper bound" class, because the member will only be whitelisted in classes that are {@code instanceof}
+ * the upper bound class. That restriction stands even if the member was inherited from another class or
+ * interface, and it wasn't even overridden in the upper bound class; the member won't be whitelisted in the
+ * class/interface where it was inherited from, if that type is more generic than the upper bound class.
+ *
+ * <p>Note that the return type of methods aren't used in any way. So if you whitelist {@code myMethod(int, int)}, and
+ * it has multiple variants with different return types (which is possible on the bytecode level), then you have
+ * whitelisted all variants of it.
+ *
+ * @since 2.3.30
+ */
+public class WhitelistMemberAccessPolicy implements MemberAccessPolicy {
+ private static final Logger LOG = Logger.getLogger("freemarker.beans");
+
+ private final MethodMatcher methodMatcher;
+ private final ConstructorMatcher constructorMatcher;
+ private final FieldMatcher fieldMatcher;
+
+ /**
+ * A condition that matches some type members. See {@link WhitelistMemberAccessPolicy} documentation for more.
+ * Exactly one of these will be non-{@code null}:
+ * {@link #getMethod()}, {@link #getConstructor()}, {@link #getField()}, {@link #getException()}.
+ *
+ * @since 2.3.30
+ */
+ public final static class MemberSelector {
+ private final Class<?> upperBoundType;
+ private final Method method;
+ private final Constructor<?> constructor;
+ private final Field field;
+ private final Exception exception;
+ private final String exceptionMemberSelectorString;
+
+ /**
+ * Use if you want to match methods similar to the specified one, in types that are {@code instanceof} of
+ * the specified upper bound type. When methods are matched, only the name and the parameter types matter.
+ */
+ public MemberSelector(Class<?> upperBoundType, Method method) {
+ NullArgumentException.check("upperBoundType", upperBoundType);
+ NullArgumentException.check("method", method);
+ this.upperBoundType = upperBoundType;
+ this.method = method;
+ this.constructor = null;
+ this.field = null;
+ this.exception = null;
+ this.exceptionMemberSelectorString = null;
+ }
+
+ /**
+ * Use if you want to match constructors similar to the specified one, in types that are {@code instanceof} of
+ * the specified upper bound type. When constructors are matched, only the parameter types matter.
+ */
+ public MemberSelector(Class<?> upperBoundType, Constructor<?> constructor) {
+ NullArgumentException.check("upperBoundType", upperBoundType);
+ NullArgumentException.check("constructor", constructor);
+ this.upperBoundType = upperBoundType;
+ this.method = null;
+ this.constructor = constructor;
+ this.field = null;
+ this.exception = null;
+ this.exceptionMemberSelectorString = null;
+ }
+
+ /**
+ * Use if you want to match fields similar to the specified one, in types that are {@code instanceof} of
+ * the specified upper bound type. When fields are matched, only the name matters.
+ */
+ public MemberSelector(Class<?> upperBoundType, Field field) {
+ NullArgumentException.check("upperBoundType", upperBoundType);
+ NullArgumentException.check("field", field);
+ this.upperBoundType = upperBoundType;
+ this.method = null;
+ this.constructor = null;
+ this.field = field;
+ this.exception = null;
+ this.exceptionMemberSelectorString = null;
+ }
+
+ /**
+ * Used to store the result of a parsing that's failed for a reason that we can skip on runtime (typically,
+ * when a missing class or member was referred).
+ *
+ * @param upperBoundType {@code null} if resolving the upper bound type itself failed.
+ * @param exception Not {@code null}
+ * @param exceptionMemberSelectorString Not {@code null}; the selector whose resolution has failed, used in
+ * the log message.
+ */
+ public MemberSelector(Class<?> upperBoundType, Exception exception, String exceptionMemberSelectorString) {
+ NullArgumentException.check("exception", exception);
+ NullArgumentException.check("exceptionMemberSelectorString", exceptionMemberSelectorString);
+ this.upperBoundType = upperBoundType;
+ this.method = null;
+ this.constructor = null;
+ this.field = null;
+ this.exception = exception;
+ this.exceptionMemberSelectorString = exceptionMemberSelectorString;
+ }
+
+ /**
+ * Maybe {@code null} if {@link #getException()} is non-{@code null}.
+ */
+ public Class<?> getUpperBoundType() {
+ return upperBoundType;
+ }
+
+ /**
+ * Maybe {@code null};
+ * set if the selector matches methods similar to the returned one, and there was no exception.
+ */
+ public Method getMethod() {
+ return method;
+ }
+
+ /**
+ * Maybe {@code null};
+ * set if the selector matches constructors similar to the returned one, and there was no exception.
+ */
+ public Constructor<?> getConstructor() {
+ return constructor;
+ }
+
+ /**
+ * Maybe {@code null};
+ * set if the selector matches fields similar to the returned one, and there was no exception.
+ */
+ public Field getField() {
+ return field;
+ }
+
+ /**
+ * Maybe {@code null}
+ */
+ public Exception getException() {
+ return exception;
+ }
+
+ /**
+ * Maybe {@code null}
+ */
+ public String getExceptionMemberSelectorString() {
+ return exceptionMemberSelectorString;
+ }
+
+ /**
+ * Parses a member selector that was specified with a string.
+ *
+ * @param classLoader
+ * Used to resolve class names in the member selectors. Generally you want to pick a class that belongs to
+ * you application (not to a 3rd party library, like FreeMarker), and then call
+ * {@link Class#getClassLoader()} on that. Note that the resolution of the classes is not lazy, and so the
+ * {@link ClassLoader} won't be stored after this method returns.
+ * @param memberSelectorString
+ * Describes the member (method, constructor, field) which you want to whitelist. Starts with the full
+ * qualified name of the member, like {@code com.example.MyClass.myMember}. Unless it's a field, the
+ * name is followed by comma separated list of the parameter types inside parentheses, like in
+ * {@code com.example.MyClass.myMember(java.lang.String, boolean)}. The parameter type names must be
+ * also full qualified names, except primitive type names. Array types must be indicated with one or
+ * more {@code []}-s after the type name. Varargs arguments shouldn't be marked with {@code ...}, but with
+ * {@code []}. In the member name, like {@code com.example.MyClass.myMember}, the class refers to the so
+ * called "upper bound class". Regarding that and inheritance rules see the class level documentation.
+ *
+ * @return The {@link MemberSelector}, which might has non-{@code null} {@link MemberSelector#exception}.
+ */
+ public static MemberSelector parse(String memberSelectorString, ClassLoader classLoader) {
+ if (memberSelectorString.contains("<") || memberSelectorString.contains(">")
+ || memberSelectorString.contains("...") || memberSelectorString.contains(";")) {
+ throw new IllegalArgumentException(
+ "Malformed whitelist entry (shouldn't contain \"<\", \">\", \"...\", or \";\"): "
+ + memberSelectorString);
+ }
+ String cleanedStr = memberSelectorString.trim().replaceAll("\\s*([\\.,\\(\\)\\[\\]])\\s*", "$1");
+
+ int postMemberNameIdx;
+ boolean hasArgList;
+ {
+ int openParenIdx = cleanedStr.indexOf('(');
+ hasArgList = openParenIdx != -1;
+ postMemberNameIdx = hasArgList ? openParenIdx : cleanedStr.length();
+ }
+
+ final int postClassDotIdx = cleanedStr.lastIndexOf('.', postMemberNameIdx);
+ if (postClassDotIdx == -1) {
+ throw new IllegalArgumentException("Malformed whitelist entry (missing dot): " + memberSelectorString);
+ }
+
+ Class<?> upperBoundClass;
+ String upperBoundClassStr = cleanedStr.substring(0, postClassDotIdx);
+ if (!isWellFormedClassName(upperBoundClassStr)) {
+ throw new IllegalArgumentException("Malformed whitelist entry (malformed upper bound class name): "
+ + memberSelectorString);
+ }
+ try {
+ upperBoundClass = classLoader.loadClass(upperBoundClassStr);
+ } catch (ClassNotFoundException e) {
+ return new MemberSelector(null, e, cleanedStr);
+ }
+
+ String memberName = cleanedStr.substring(postClassDotIdx + 1, postMemberNameIdx);
+ if (!isWellFormedJavaIdentifier(memberName)) {
+ throw new IllegalArgumentException(
+ "Malformed whitelist entry (malformed member name): " + memberSelectorString);
+ }
+
+ if (hasArgList) {
+ if (cleanedStr.charAt(cleanedStr.length() - 1) != ')') {
+ throw new IllegalArgumentException("Malformed whitelist entry (missing closing ')'): "
+ + memberSelectorString);
+ }
+ String argsSpec = cleanedStr.substring(postMemberNameIdx + 1, cleanedStr.length() - 1);
+ StringTokenizer tok = new StringTokenizer(argsSpec, ",");
+ int argCount = tok.countTokens();
+ Class<?>[] argTypes = new Class[argCount];
+ for (int i = 0; i < argCount; i++) {
+ String argClassName = tok.nextToken();
+ int arrayDimensions = 0;
+ while (argClassName.endsWith("[]")) {
+ arrayDimensions++;
+ argClassName = argClassName.substring(0, argClassName.length() - 2);
+ }
+ Class<?> argClass;
+ Class<?> primArgClass = ClassUtil.resolveIfPrimitiveTypeName(argClassName);
+ if (primArgClass != null) {
+ argClass = primArgClass;
+ } else {
+ if (!isWellFormedClassName(argClassName)) {
+ throw new IllegalArgumentException(
+ "Malformed whitelist entry (malformed argument class name): " + memberSelectorString);
+ }
+ try {
+ argClass = classLoader.loadClass(argClassName);
+ } catch (ClassNotFoundException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ } catch (SecurityException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ }
+ }
+ argTypes[i] = ClassUtil.getArrayClass(argClass, arrayDimensions);
+ }
+ try {
+ return memberName.equals(upperBoundClass.getSimpleName())
+ ? new MemberSelector(upperBoundClass, upperBoundClass.getConstructor(argTypes))
+ : new MemberSelector(upperBoundClass, upperBoundClass.getMethod(memberName, argTypes));
+ } catch (NoSuchMethodException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ } catch (SecurityException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ }
+ } else {
+ try {
+ return new MemberSelector(upperBoundClass, upperBoundClass.getField(memberName));
+ } catch (NoSuchFieldException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ } catch (SecurityException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ }
+ }
+ }
+
+ /**
+ * Convenience method to parse all member selectors in the collection; see {@link #parse(String, ClassLoader)}.
+ */
+ public static List<MemberSelector> parse(Collection<String> memberSelectors,
+ ClassLoader classLoader) {
+ List<MemberSelector> parsedMemberSelectors = new ArrayList<MemberSelector>(memberSelectors.size());
+ for (String memberSelector : memberSelectors) {
+ parsedMemberSelectors.add(parse(memberSelector, classLoader));
+ }
+ return parsedMemberSelectors;
+ }
+ }
+
+ public WhitelistMemberAccessPolicy(Collection<MemberSelector> memberSelectors) {
+ methodMatcher = new MethodMatcher();
+ constructorMatcher = new ConstructorMatcher();
+ fieldMatcher = new FieldMatcher();
+ for (MemberSelector memberSelector : memberSelectors) {
+ Class<?> upperBoundClass = memberSelector.upperBoundType;
+ if (memberSelector.exception != null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Member selector ignored due to error: " + memberSelector.getExceptionMemberSelectorString(),
+ memberSelector.exception);
+ }
+ } else if (memberSelector.constructor != null) {
+ constructorMatcher.addMatching(upperBoundClass, memberSelector.constructor);
+ } else if (memberSelector.method != null) {
+ methodMatcher.addMatching(upperBoundClass, memberSelector.method);
+ } else if (memberSelector.field != null) {
+ fieldMatcher.addMatching(upperBoundClass, memberSelector.field);
+ } else {
+ throw new AssertionError();
+ }
+ }
+ }
+
+ public ClassMemberAccessPolicy forClass(final Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return methodMatcher.matches(contextClass, method)
+ || _MethodUtil.getInheritableAnnotation(contextClass, method, TemplateAccessible.class) != null;
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return constructorMatcher.matches(contextClass, constructor)
+ || _MethodUtil.getInheritableAnnotation(contextClass, constructor, TemplateAccessible.class)
+ != null;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return fieldMatcher.matches(contextClass, field)
+ || _MethodUtil.getInheritableAnnotation(contextClass, field, TemplateAccessible.class) != null;
+ }
+ };
+ }
+
+ private static boolean isWellFormedClassName(String s) {
+ if (s.length() == 0) {
+ return false;
+ }
+ int identifierStartIdx = 0;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (i == identifierStartIdx) {
+ if (!Character.isJavaIdentifierStart(c)) {
+ return false;
+ }
+ } else if (c == '.' && i != s.length() - 1) {
+ identifierStartIdx = i + 1;
+ } else {
+ if (!Character.isJavaIdentifierPart(c)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private static boolean isWellFormedJavaIdentifier(String s) {
+ if (s.length() == 0) {
+ return false;
+ }
+ if (!Character.isJavaIdentifierStart(s.charAt(0))) {
+ return false;
+ }
+ for (int i = 1; i < s.length(); i++) {
+ if (!Character.isJavaIdentifierPart(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/_MethodUtil.java b/src/main/java/freemarker/ext/beans/_MethodUtil.java
index 782b944..9f743bc 100644
--- a/src/main/java/freemarker/ext/beans/_MethodUtil.java
+++ b/src/main/java/freemarker/ext/beans/_MethodUtil.java
@@ -18,7 +18,9 @@
*/
package freemarker.ext.beans;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
@@ -317,4 +319,143 @@
.toString();
}
+ /**
+ * Similar to {@link Method#getAnnotation(Class)}, but will also search the annotation in the implemented
+ * interfaces and in the ancestor classes.
+ */
+ public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Method method, Class<T> annotationClass) {
+ T result = method.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ return getInheritableMethodAnnotation(
+ contextClass, method.getName(), method.getParameterTypes(), true, annotationClass);
+ }
+
+ private static <T extends Annotation> T getInheritableMethodAnnotation(
+ Class<?> contextClass, String methodName, Class<?>[] methodParamTypes,
+ boolean skipCheckingDirectMethod,
+ Class<T> annotationClass) {
+ if (!skipCheckingDirectMethod) {
+ Method similarMethod;
+ try {
+ similarMethod = contextClass.getMethod(methodName, methodParamTypes);
+ } catch (NoSuchMethodException e) {
+ similarMethod = null;
+ }
+ if (similarMethod != null) {
+ T result = similarMethod.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ for (Class<?> anInterface : contextClass.getInterfaces()) {
+ if (!anInterface.getName().startsWith("java.")) {
+ Method similarInterfaceMethod;
+ try {
+ similarInterfaceMethod = anInterface.getMethod(methodName, methodParamTypes);
+ } catch (NoSuchMethodException e) {
+ similarInterfaceMethod = null;
+ }
+ if (similarInterfaceMethod != null) {
+ T result = similarInterfaceMethod.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ }
+ Class<?> superClass = contextClass.getSuperclass();
+ if (superClass == Object.class || superClass == null) {
+ return null;
+ }
+ return getInheritableMethodAnnotation(superClass, methodName, methodParamTypes, false, annotationClass);
+ }
+
+ /**
+ * Similar to {@link Constructor#getAnnotation(Class)}, but will also search the annotation in the implemented
+ * interfaces and in the ancestor classes.
+ */
+ public static <T extends Annotation> T getInheritableAnnotation(
+ Class<?> contextClass, Constructor<?> constructor, Class<T> annotationClass) {
+ T result = constructor.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+
+ Class<?>[] paramTypes = constructor.getParameterTypes();
+ while (true) {
+ contextClass = contextClass.getSuperclass();
+ if (contextClass == Object.class || contextClass == null) {
+ return null;
+ }
+ try {
+ constructor = contextClass.getConstructor(paramTypes);
+ } catch (NoSuchMethodException e) {
+ constructor = null;
+ }
+ if (constructor != null) {
+ result = constructor.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ }
+
+ /**
+ * Similar to {@link Field#getAnnotation(Class)}, but will also search the annotation in the implemented
+ * interfaces and in the ancestor classes.
+ */
+ public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Field field, Class<T> annotationClass) {
+ T result = field.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ return getInheritableFieldAnnotation(
+ contextClass, field.getName(), true, annotationClass);
+ }
+
+ private static <T extends Annotation> T getInheritableFieldAnnotation(
+ Class<?> contextClass, String fieldName,
+ boolean skipCheckingDirectField,
+ Class<T> annotationClass) {
+ if (!skipCheckingDirectField) {
+ Field similarField;
+ try {
+ similarField = contextClass.getField(fieldName);
+ } catch (NoSuchFieldException e) {
+ similarField = null;
+ }
+ if (similarField != null) {
+ T result = similarField.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ for (Class<?> anInterface : contextClass.getInterfaces()) {
+ if (!anInterface.getName().startsWith("java.")) {
+ Field similarInterfaceField;
+ try {
+ similarInterfaceField = anInterface.getField(fieldName);
+ } catch (NoSuchFieldException e) {
+ similarInterfaceField = null;
+ }
+ if (similarInterfaceField != null) {
+ T result = similarInterfaceField.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ }
+ Class<?> superClass = contextClass.getSuperclass();
+ if (superClass == Object.class || superClass == null) {
+ return null;
+ }
+ return getInheritableFieldAnnotation(superClass, fieldName, false, annotationClass);
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java
index 4c5a39d..8333a00 100644
--- a/src/main/java/freemarker/template/DefaultObjectWrapper.java
+++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java
@@ -32,6 +32,7 @@
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperConfiguration;
+import freemarker.ext.beans.DefaultMemberAccessPolicy;
import freemarker.ext.beans.EnumerationModel;
import freemarker.ext.dom.NodeModel;
import freemarker.log.Logger;
@@ -252,7 +253,8 @@
* Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type,
* or for a W3C DOM node. In its default implementation, W3C {@link Node}-s will be wrapped as {@link NodeModel}-s
* (allows DOM tree traversal), Jython objects will be delegated to the {@code JythonWrapper}, others will be
- * wrapped using {@link BeansWrapper#wrap(Object)}.
+ * wrapped using {@link BeansWrapper#wrap(Object)}. Note that if {@link #getMemberAccessPolicy()} doesn't return
+ * a {@link DefaultMemberAccessPolicy}, then Jython wrapper will be skipped for security reasons.
*
* <p>
* When you override this method, you should first decide if you want to wrap the object in a custom way (and if so
@@ -263,8 +265,10 @@
if (obj instanceof Node) {
return wrapDomNode(obj);
}
- if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
- return JYTHON_WRAPPER.wrap(obj);
+ if (getMemberAccessPolicy() instanceof DefaultMemberAccessPolicy) {
+ if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
+ return JYTHON_WRAPPER.wrap(obj);
+ }
}
return super.wrap(obj);
}
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index ad19750..95fd3b8 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -21,8 +21,11 @@
import java.io.IOException;
import java.io.InputStream;
+import java.lang.reflect.Array;
import java.net.URL;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Properties;
import java.util.Set;
@@ -88,7 +91,39 @@
// Fall back to the defining class loader of the FreeMarker classes
return Class.forName(className);
}
-
+
+ private static final Map<String, Class<?>> PRIMITIVE_CLASSES_BY_NAME;
+ static {
+ PRIMITIVE_CLASSES_BY_NAME = new HashMap<String, Class<?>>();
+ PRIMITIVE_CLASSES_BY_NAME.put("boolean", boolean.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("byte", byte.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("char", char.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("short", short.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("int", int.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("long", long.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("float", float.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("double", double.class);
+ }
+
+ /**
+ * Returns the {@link Class} for a primitive type name, or {@code null} if it's not the name of a primitive type.
+ *
+ * @since 2.3.30
+ */
+ public static Class<?> resolveIfPrimitiveTypeName(String typeName) {
+ return PRIMITIVE_CLASSES_BY_NAME.get(typeName);
+ }
+
+ /**
+ * Returns the array type that corresponds to the element type and the given number of array dimensions.
+ * If the dimension is 0, it just returns the element type as is.
+ *
+ * @since 2.3.30
+ */
+ public static Class<?> getArrayClass(Class<?> elementType, int dimensions) {
+ return dimensions == 0 ? elementType : Array.newInstance(elementType, new int[dimensions]).getClass();
+ }
+
/**
* Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}.
*
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 4b10a92..a2732b6 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,10 +20,7 @@
<book conformance="docgen" version="5.0" xml:lang="en"
xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:ns5="http://www.w3.org/2000/svg"
- xmlns:ns4="http://www.w3.org/1998/Math/MathML"
- xmlns:ns3="http://www.w3.org/1999/xhtml"
- xmlns:ns="http://docbook.org/ns/docbook">
+>
<info>
<title>Apache FreeMarker Manual</title>
@@ -28887,10 +28884,11 @@
<answer>
<para>In general you shouldn't allow that, unless those users are
- system administrators or other trusted personnel. Consider
- templates as part of the source code just like
- <literal>*.java</literal> files are. If you still want to allow
- users to upload templates, here are what to consider:</para>
+ application developers, system administrators, or other highly
+ trusted personnel. Consider templates as part of the source code
+ just like <literal>*.java</literal> files are. If you still want
+ to allow users to upload templates, here's what to
+ consider:</para>
<itemizedlist>
<listitem>
@@ -28915,11 +28913,17 @@
<literal>List</literal>-s, <literal>Array</literal>-s,
<literal>String</literal>-s, <literal>Number</literal>-s,
<literal>Boolean</literal>-s and <literal>Date</literal>-s.
- For many application though that's too restrictive, and
- instead you need to implement your own extremely restrictive
- <literal>ObjectWrapper</literal>, which, for example, only
- exposes those members of POJO-s that were explicitly marked to
- be safe (opt-in approach).</para>
+ But for many application that's too restrictive, and instead
+ you have to create a
+ <literal>WhitelistMemberAccessPolicy</literal>, and create a
+ <literal>DefaultObjectWrapper</literal> (or other
+ <literal>BeansWrapper</literal> subclass that you would use)
+ that uses that. See the Java API documentation of
+ <literal>WhitelistMemberAccessPolicy</literal> for more. (Or,
+ you can roll your own <literal>MemberAccessPolicy</literal>
+ implementation, or even your own restrictive
+ <literal>ObjectWrapper</literal> implementation of
+ course.)</para>
<para>Also, don't forget about the <link
linkend="ref_buitin_api_and_has_api"><literal>?api</literal>
@@ -28935,16 +28939,23 @@
the <literal>ObjectWrapper</literal> is still in control, as
it decides what objects support <literal>?api</literal>, and
what will <literal>?api</literal> expose for them (it usually
- exposes the same as for a generic POJO).</para>
+ exposes the same as for a generic POJO). Members not allowed
+ by the <literal>MemberAccessPolicy</literal> also won't be
+ visible with <literal>?api</literal> (assuming you are using a
+ well behaving <literal>ObjectWrapper</literal>, like
+ <literal>DefaultObjectWrapper</literal> is, hopefully.)
+ </para>
<para>Last not least, some maybe aware of that the standard
object wrappers filters out some well known
<quote>unsafe</quote> methods, like
- <literal>System.exit</literal>. Do not ever rely on this as
- your only line of defense, since it only blocks the methods
- that's in a predefined list. Thus, for example, if a new Java
- version adds a new problematic method, it won't be filtered
- out.</para>
+ <literal>System.exit</literal>. Do not ever rely on that,
+ since it only blocks the methods that's in a small predefined
+ list (for some historical reasons). The standard Java API is
+ huge and ever growing, and then there are the 3rd party
+ libraries, and the API-s of your own application. Clearly it's
+ impossible to blacklist all the problematic members in
+ those.</para>
</listitem>
<listitem>
@@ -28986,7 +28997,13 @@
<literal>TemplateClassResolver</literal> that restricts the
accessible classes (possibly based on which template asks for
them), such as
- <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.</para>
+ <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.
+ Note that if, and only if your
+ <literal>ObjectWrapper</literal> is a
+ <literal>BeansWrapper</literal> or a subclass of it (typically
+ <literal>DefaultObjectWrapper</literal>), constructors not
+ allowed by the <literal>MemberAccessPolicy</literal> also
+ won't be accessible for <literal>?new</literal>. </para>
</listitem>
<listitem>
@@ -29197,6 +29214,23 @@
<listitem>
<para>Added
+ <literal>freemarker.ext.beans.WhitelistMemberAccessPolicy</literal>,
+ which is a <literal>MemberAccessPolicy</literal> for use cases
+ where you want to allow editing templates to users who shouldn't
+ have the same rights as the developers (the same rights as the
+ Java application). Earlier, the only out of the box solution for
+ that was <literal>SimpleObjectWrapper</literal>, but that's too
+ restrictive for most applications where FreeMarker is used.
+ <literal>WhitelistMemberAccessPolicy</literal> works with
+ <literal>DefaultObjectWrapper</literal> (or any other
+ <literal>BeansWrapper</literal>), allowing you to use all
+ features of it, but it will only allow accessing members that
+ were explicitly listed by the developers, or was annotated with
+ <literal>@TemplateAccessible</literal>.</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
index 355a769..eafbaf2 100644
--- a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
+++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -22,24 +22,32 @@
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
+import java.io.IOException;
+import java.io.StringWriter;
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 java.util.Map;
import org.junit.Test;
+import com.google.common.collect.ImmutableMap;
+
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.ObjectWrapperAndUnwrapper;
import freemarker.template.SimpleNumber;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
public class DefaultObjectWrapperMemberAccessPolicyTest {
@@ -166,7 +174,7 @@
public void testMethodsWithCustomMemberAccessPolicy() throws TemplateModelException {
DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
owb.setMemberAccessPolicy(new MemberAccessPolicy() {
- public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
return new ClassMemberAccessPolicy() {
public boolean isMethodExposed(Method method) {
String name = method.getName();
@@ -208,7 +216,7 @@
DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
owb.setExposeFields(true);
owb.setMemberAccessPolicy(new MemberAccessPolicy() {
- public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
return new ClassMemberAccessPolicy() {
public boolean isMethodExposed(Method method) {
return true;
@@ -238,7 +246,7 @@
public void testGenericGetWithCustomMemberAccessPolicy() throws TemplateModelException {
DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
owb.setMemberAccessPolicy(new MemberAccessPolicy() {
- public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
return new ClassMemberAccessPolicy() {
public boolean isMethodExposed(Method method) {
return false;
@@ -264,7 +272,7 @@
public void testConstructorsWithCustomMemberAccessPolicy() throws TemplateModelException {
DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
owb.setMemberAccessPolicy(new MemberAccessPolicy() {
- public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
return new ClassMemberAccessPolicy() {
public boolean isMethodExposed(Method method) {
return true;
@@ -305,6 +313,93 @@
Collections.singletonList(new SimpleNumber(1))).getClass());
}
+ @Test
+ public void testMemberAccessPolicyAndApiBI() throws IOException, TemplateException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return method.getName().equals("size");
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return true;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ Map<String, Object> dataModel = ImmutableMap.<String, Object>of("m", ImmutableMap.of("k", "v"));
+
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setObjectWrapper(ow);
+ cfg.setAPIBuiltinEnabled(true);
+ Template template = new Template(null, "size=${m?api.size()} get=${(m?api.get('k'))!'hidden'}", cfg);
+
+ {
+ StringWriter out = new StringWriter();
+ template.process(dataModel, out);
+ assertEquals("size=1 get=hidden", out.toString());
+ }
+
+ cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build());
+ {
+ StringWriter out = new StringWriter();
+ template.process(dataModel, out);
+ assertEquals("size=1 get=v", out.toString());
+ }
+ }
+
+ @Test
+ public void testMemberAccessPolicyAndNewBI() throws IOException, TemplateException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return true;
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return constructor.getDeclaringClass().equals(CustomModel.class);
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setObjectWrapper(ow);
+ cfg.setAPIBuiltinEnabled(true);
+ Template template = new Template(null,
+ "${'" + CustomModel.class.getName() + "'?new()} "
+ + "<#attempt>${'" + OtherCustomModel.class.getName() + "'?new()}<#recover>failed</#attempt>",
+ cfg);
+
+ {
+ StringWriter out = new StringWriter();
+ template.process(null, out);
+ assertEquals("1 failed", out.toString());
+ }
+
+ cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build());
+ {
+ StringWriter out = new StringWriter();
+ template.process(null, out);
+ assertEquals("1 2", out.toString());
+ }
+ }
+
private static DefaultObjectWrapper createDefaultMemberAccessPolicyObjectWrapper() {
return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
}
@@ -406,4 +501,16 @@
}
}
+ public static class CustomModel implements TemplateNumberModel {
+ public Number getAsNumber() {
+ return 1;
+ }
+ }
+
+ public static class OtherCustomModel implements TemplateNumberModel {
+ public Number getAsNumber() {
+ return 2;
+ }
+ }
+
}
diff --git a/src/test/java/freemarker/ext/beans/MethodMatcherTest.java b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java
new file mode 100644
index 0000000..9ac9ca9
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class MethodMatcherTest {
+
+ @Test
+ public void testReturnTypeOverload() throws NoSuchMethodException {
+ MethodMatcher matcher = new MethodMatcher();
+ Method genericM = TestReturnTypeOverloadGeneric.class.getMethod("m");
+ assertEquals(Object.class, genericM.getReturnType());
+ matcher.addMatching(TestReturnTypeOverloadGeneric.class, genericM);
+
+ Method stringM = TestReturnTypeOverloadString.class.getMethod("m");
+ assertEquals(String.class, stringM.getReturnType());
+
+ assertTrue(matcher.matches(TestReturnTypeOverloadGeneric.class, genericM));
+ assertTrue(matcher.matches(TestReturnTypeOverloadString.class, genericM));
+ assertTrue(matcher.matches(TestReturnTypeOverloadString.class, stringM));
+ }
+
+ public static class TestReturnTypeOverloadGeneric<T> {
+ public T m() {
+ return null;
+ };
+ }
+
+ public static class TestReturnTypeOverloadString extends TestReturnTypeOverloadGeneric<String> {
+ public String m() {
+ return "";
+ };
+ }
+
+ /** Mostly to test upper bound classes. */
+ @Test
+ public void testInheritance() throws NoSuchMethodException {
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC2.class.getMethod("m1");
+ assertEquals(m, TestInheritanceC1.class.getMethod("m1"));
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC2.class.getMethod("m2");
+ assertNotEquals(m, TestInheritanceC1.class.getMethod("m2"));
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ {
+ // m2 again, but with a non-same-instance but "equal" method.
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC1.class.getMethod("m2");
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC2.class.getMethod("m3");
+ assertEquals(m, TestInheritanceC1.class.getMethod("m3"));
+ assertNotEquals(m, TestInheritanceC3.class.getMethod("m3"));
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ }
+
+ public static class TestInheritanceC1 {
+ public void m1() {
+ }
+
+ public void m2() {
+ }
+
+ public void m3() {
+ }
+ }
+
+ public static class TestInheritanceC2 extends TestInheritanceC1 {
+ @Override
+ public void m2() {
+ }
+ }
+
+ public static class TestInheritanceC3 extends TestInheritanceC2 {
+ @Override
+ public void m3() {
+ }
+ }
+
+ /** Mostly to test when same method associated to multiple unrelated classes. */
+ @Test
+ public void testInheritance2() throws NoSuchMethodException {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = Runnable.class.getMethod("run");
+ matcher.addMatching(TestInheritance2SafeRunnable1.class, m);
+ matcher.addMatching(TestInheritance2SafeRunnable2.class, m);
+
+ assertTrue(matcher.matches(
+ TestInheritance2SafeRunnable1.class, TestInheritance2SafeRunnable1.class.getMethod("run")));
+ assertTrue(matcher.matches(
+ TestInheritance2SafeRunnable2.class, TestInheritance2SafeRunnable2.class.getMethod("run")));
+ assertFalse(matcher.matches(
+ TestInheritance2UnsafeRunnable.class, TestInheritance2UnsafeRunnable.class.getMethod("run")));
+ }
+
+ public static class TestInheritance2SafeRunnable1 implements Runnable {
+ public void run() {
+ }
+ }
+
+ public static class TestInheritance2SafeRunnable2 implements Runnable {
+ public void run() {
+ }
+ }
+
+ public static class TestInheritance2UnsafeRunnable implements Runnable {
+ public void run() {
+ }
+ }
+
+ @Test
+ public void testOverloads() throws NoSuchMethodException {
+ Method mInt = TestOverloads.class.getMethod("m", int.class);
+ Method mIntInt = TestOverloads.class.getMethod("m", int.class, int.class);
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ matcher.addMatching(TestOverloads.class, mInt);
+ assertTrue(matcher.matches(TestOverloads.class, mInt));
+ assertFalse(matcher.matches(TestOverloads.class, mIntInt));
+ }
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ matcher.addMatching(TestOverloads.class, mIntInt);
+ assertFalse(matcher.matches(TestOverloads.class, mInt));
+ assertTrue(matcher.matches(TestOverloads.class, mIntInt));
+ }
+ }
+
+ public static class TestOverloads {
+ public void m(int x) {
+ }
+
+ public void m(int x, int y) {
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MethodUtilTest.java b/src/test/java/freemarker/ext/beans/MethodUtilTest.java
new file mode 100644
index 0000000..24291b3
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodUtilTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.junit.Assert.*;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class MethodUtilTest {
+
+ @Test
+ public void testMethodBasic() throws NoSuchMethodException, NoSuchFieldException {
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getMethod("m1"), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getMethod("m2"), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getConstructor(int.class), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getConstructor(int.class, int.class), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getField("f1"), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getField("f3"), TemplateAccessible.class));
+ }
+
+ @Test
+ public void testMethodInheritance() throws NoSuchMethodException, NoSuchFieldException {
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m1"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m2"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m3"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m4"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m5"), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getConstructor(int.class), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getConstructor(), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f1"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f2"), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f3"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f4"), TemplateAccessible.class));
+ }
+
+ @Test
+ public void testMethodInheritanceWithSyntheticMethod() {
+ for (Method method : D2.class.getMethods()) {
+ if (method.getName().equals("m1")) {
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, method, TemplateAccessible.class));
+ }
+ }
+ }
+
+ static public class C1 implements Serializable {
+ @TemplateAccessible
+ public int f1;
+
+ @TemplateAccessible
+ public int f2;
+
+ public int f3;
+
+ public int f4;
+
+ @TemplateAccessible
+ public C1(int x) {}
+
+ public C1(int x, int y) {}
+
+ @TemplateAccessible
+ public void m1() {}
+
+ public void m2() {}
+
+ public void m3() {}
+
+ @TemplateAccessible
+ public void m4() {}
+
+ @TemplateAccessible
+ public void m5() {}
+ }
+
+ static public class C2 extends C1 implements I1 {
+ public long f2;
+
+ public C2() {
+ super(0);
+ }
+
+ public C2(int x) {
+ super(x);
+ }
+
+ @Override
+ public void m1() {}
+
+ @TemplateAccessible
+ @Override
+ public void m3() {}
+ }
+
+ public interface I1 {
+ @TemplateAccessible
+ int f4 = 0;
+
+ @TemplateAccessible
+ void m2();
+
+ void m5();
+ }
+
+ public static class D1<T> {
+ @TemplateAccessible
+ public T m1() { return null; }
+ }
+
+ public static class D2 extends D1<String> {
+ @Override
+ public String m1() { return ""; }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java
new file mode 100644
index 0000000..0edee07
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/WhitelistMemberAccessPolicyTest.java
@@ -0,0 +1,558 @@
+/*
+ * 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.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModel;
+
+public class WhitelistMemberAccessPolicyTest {
+
+ @Test
+ public void testEmpty() throws NoSuchMethodException, NoSuchFieldException {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy();
+ ClassMemberAccessPolicy classPolicy = policy.forClass(C1.class);
+ assertFalse(classPolicy.isConstructorExposed(C1.class.getConstructor()));
+ assertFalse(classPolicy.isMethodExposed(C1.class.getMethod("m1")));
+ assertFalse(classPolicy.isFieldExposed(C1.class.getField("f1")));
+ }
+
+ @Test
+ public void testBasics() throws NoSuchMethodException, NoSuchFieldException {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ C1.class.getName() + "." + C1.class.getSimpleName() + "()",
+ C1.class.getName() + ".m1()",
+ C1.class.getName() + ".m2(int)",
+ C1.class.getName() + ".f1");
+
+ {
+ ClassMemberAccessPolicy c1Policy = policy.forClass(C1.class);
+ assertTrue(c1Policy.isConstructorExposed(C1.class.getConstructor()));
+ assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m1")));
+ assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m2", int.class)));
+ assertTrue(c1Policy.isFieldExposed(C1.class.getField("f1")));
+ }
+
+ {
+ ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+ assertFalse(d1Policy.isMethodExposed(D1.class.getMethod("m1")));
+ assertFalse(d1Policy.isFieldExposed(D1.class.getField("f1")));
+ }
+ }
+
+ @Test
+ public void testInheritanceAndMoreOverloads() throws NoSuchMethodException, NoSuchFieldException {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ C1.class.getName() + ".m2(int)",
+ C1.class.getName() + ".f1",
+ C2.class.getName() + "." + C2.class.getSimpleName() + "(int)",
+ C2.class.getName() + ".m1()",
+ C2.class.getName() + ".m2(boolean)",
+ C3.class.getName() + ".f2",
+ C3.class.getName() + "." + C3.class.getSimpleName() + "()",
+ C3.class.getName() + ".m4()",
+ C3.class.getName() + ".f3"
+ );
+ ClassMemberAccessPolicy c1Policy = policy.forClass(C1.class);
+ ClassMemberAccessPolicy c2Policy = policy.forClass(C2.class);
+ ClassMemberAccessPolicy c3Policy = policy.forClass(C3.class);
+
+ assertTrue(c1Policy.isMethodExposed(C1.class.getMethod("m2", int.class)));
+ assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m2", int.class)));
+ assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m2", int.class)));
+
+ assertTrue(c1Policy.isFieldExposed(C1.class.getField("f1")));
+ assertTrue(c2Policy.isFieldExposed(C2.class.getField("f1")));
+ assertTrue(c3Policy.isFieldExposed(C3.class.getField("f1")));
+
+ assertFalse(c1Policy.isConstructorExposed(C1.class.getConstructor(int.class)));
+ assertTrue(c2Policy.isConstructorExposed(C2.class.getConstructor(int.class)));
+ assertTrue(c3Policy.isConstructorExposed(C3.class.getConstructor(int.class)));
+
+ assertFalse(c1Policy.isMethodExposed(C1.class.getMethod("m1")));
+ assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m1")));
+ assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m1")));
+
+ assertFalse(c1Policy.isMethodExposed(C2.class.getMethod("m2", boolean.class))); // Doesn't exist in C1
+ assertTrue(c2Policy.isMethodExposed(C2.class.getMethod("m2", boolean.class)));
+ assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m2", boolean.class)));
+
+ assertFalse(c1Policy.isFieldExposed(C1.class.getField("f2")));
+ assertFalse(c2Policy.isFieldExposed(C2.class.getField("f2")));
+ assertTrue(c3Policy.isFieldExposed(C3.class.getField("f2")));
+
+ assertFalse(c1Policy.isConstructorExposed(C1.class.getConstructor()));
+ assertFalse(c2Policy.isConstructorExposed(C1.class.getConstructor())); // Doesn't exist in C2
+ assertTrue(c3Policy.isConstructorExposed(C3.class.getConstructor()));
+
+ assertFalse(c1Policy.isMethodExposed(C2.class.getMethod("m4"))); // Doesn't exist in C1
+ assertFalse(c2Policy.isMethodExposed(C2.class.getMethod("m4")));
+ assertTrue(c3Policy.isMethodExposed(C3.class.getMethod("m4")));
+
+ assertFalse(c1Policy.isFieldExposed(C2.class.getField("f3"))); // Doesn't exist in C1
+ assertFalse(c2Policy.isFieldExposed(C2.class.getField("f3")));
+ assertTrue(c3Policy.isFieldExposed(C3.class.getField("f3")));
+ }
+
+ @Test
+ public void testInterfaces() throws NoSuchMethodException, NoSuchFieldException {
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ I1.class.getName() + ".m1()",
+ I1.class.getName() + ".f1"
+ );
+ ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+ ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+ ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+ ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+ assertTrue(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(d1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+ }
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ I1Sub.class.getName() + ".m1()",
+ I1Sub.class.getName() + ".m2()",
+ I1Sub.class.getName() + ".f1"
+ );
+ ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+ ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+ ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+ ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+ assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertFalse(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertFalse(d1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertFalse(d2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertTrue(e1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertTrue(e2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertFalse(d1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertFalse(d2Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+ }
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ I1.class.getName() + ".m1()",
+ I1.class.getName() + ".f1"
+ );
+ ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+ ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+ ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+ ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+ assertTrue(d1Policy.isMethodExposed(I1Sub.class.getMethod("m1")));
+ assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertFalse(d1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertFalse(d2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertFalse(e1Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertFalse(e2Policy.isMethodExposed(I1Sub.class.getMethod("m2")));
+ assertTrue(d1Policy.isFieldExposed(I1Sub.class.getField("f1")));
+ assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+ }
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ D2.class.getName() + ".m1()",
+ D2.class.getName() + ".f1"
+ );
+ ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+ ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+ assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertFalse(d1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+ }
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ I1Sub.class.getName() + ".m1()",
+ D2.class.getName() + ".m1()",
+ I1Sub.class.getName() + ".m2()",
+ J1.class.getName() + ".m2()",
+ I1.class.getName() + ".f1",
+ I1Sub.class.getName() + ".f1"
+ );
+ ClassMemberAccessPolicy d1Policy = policy.forClass(D1.class);
+ ClassMemberAccessPolicy d2Policy = policy.forClass(D2.class);
+ ClassMemberAccessPolicy e1Policy = policy.forClass(E1.class);
+ ClassMemberAccessPolicy e2Policy = policy.forClass(E2.class);
+ ClassMemberAccessPolicy f1Policy = policy.forClass(F1.class);
+ assertFalse(d1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(d2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e1Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertTrue(e2Policy.isMethodExposed(I1.class.getMethod("m1")));
+ assertFalse(d1Policy.isMethodExposed(J1.class.getMethod("m2")));
+ assertFalse(d2Policy.isMethodExposed(J1.class.getMethod("m2")));
+ assertTrue(e1Policy.isMethodExposed(J1.class.getMethod("m2")));
+ assertTrue(e2Policy.isMethodExposed(J1.class.getMethod("m2")));
+ assertTrue(f1Policy.isMethodExposed(J1.class.getMethod("m2")));
+ assertTrue(d1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(d2Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e1Policy.isFieldExposed(I1.class.getField("f1")));
+ assertTrue(e2Policy.isFieldExposed(I1.class.getField("f1")));
+ }
+ }
+
+ @Test
+ public void testArrayArgs() throws NoSuchMethodException {
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ CArrayArgs.class.getName() + ".m1(java.lang.String)",
+ CArrayArgs.class.getName() + ".m1(java.lang.String[])",
+ CArrayArgs.class.getName() + ".m1(java.lang.String[][])",
+ CArrayArgs.class.getName() + ".m2(" + C1.class.getName() + "[])",
+ CArrayArgs.class.getName() + ".m2("
+ + C1.class.getName() + "[], "
+ + C1.class.getName() + "[], "
+ + C1.class.getName() + ")"
+ );
+ ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class);
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class)));
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[].class)));
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[][].class)));
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m2", C1[].class)));
+ assertTrue(classPolicy.isMethodExposed(
+ CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class)));
+ }
+ {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ CArrayArgs.class.getName() + ".m1(java.lang.String)",
+ CArrayArgs.class.getName() + ".m1(java.lang.String[][])"
+ );
+ ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class);
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class)));
+ assertFalse(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[].class)));
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String[][].class)));
+ assertFalse(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m2", C1[].class)));
+ assertFalse(classPolicy.isMethodExposed(
+ CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class)));
+ }
+ }
+
+ @Test
+ public void memberSelectorParserIgnoresWhitespace() throws NoSuchMethodException {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ (CArrayArgs.class.getName() + ".m1(java.lang.String)").replace(".", "\n\t. "),
+ CArrayArgs.class.getName() + ".m2("
+ + C1.class.getName() + " [ ]\t,"
+ + C1.class.getName() + "[] ,\n "
+ + C1.class.getName() + " )"
+ );
+ ClassMemberAccessPolicy classPolicy = policy.forClass(CArrayArgs.class);
+ assertTrue(classPolicy.isMethodExposed(CArrayArgs.class.getMethod("m1", String.class)));
+ assertTrue(classPolicy.isMethodExposed(
+ CArrayArgs.class.getMethod("m2", C1[].class, C1[].class, C1.class)));
+ }
+
+ @Test
+ public void memberSelectorParsingErrorsTest() {
+ try {
+ newWhitelistMemberAccessPolicy("foo()");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("missing dot"));
+ }
+ try {
+ newWhitelistMemberAccessPolicy("com.example.Foo-bar.m()");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("malformed upper bound class name"));
+ }
+ try {
+ newWhitelistMemberAccessPolicy("java.util.Date.m-x()");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("malformed member name"));
+ }
+ try {
+ newWhitelistMemberAccessPolicy("java.util.Date.to string()");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("malformed member name"));
+ }
+ try {
+ newWhitelistMemberAccessPolicy("java.util.Date.toString(");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("missing closing ')'"));
+ }
+ try {
+ newWhitelistMemberAccessPolicy("java.util.Date.m(com.x-y)");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("malformed argument class name"));
+ }
+ try {
+ newWhitelistMemberAccessPolicy("java.util.Date.m(int[)");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("malformed argument class name"));
+ }
+ }
+
+ @Test
+ public void testAnnotation() throws NoSuchFieldException, NoSuchMethodException {
+ WhitelistMemberAccessPolicy policy = newWhitelistMemberAccessPolicy(
+ CAnnotationsTest2.class.getName() + ".f2",
+ CAnnotationsTest2.class.getName() + ".f3",
+ CAnnotationsTest2.class.getName() + ".m2()",
+ CAnnotationsTest2.class.getName() + ".m3()",
+ CAnnotationsTest2.class.getName() + "." + CAnnotationsTest2.class.getSimpleName() + "(int)",
+ CAnnotationsTest2.class.getName() + "." + CAnnotationsTest2.class.getSimpleName() + "(int, int)"
+ );
+ ClassMemberAccessPolicy classPolicy = policy.forClass(CAnnotationsTest2.class);
+
+ assertFalse(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f1")));
+ assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f2")));
+ assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f3")));
+ assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f4")));
+ assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f5")));
+ assertTrue(classPolicy.isFieldExposed(CAnnotationsTest2.class.getField("f6")));
+
+ assertFalse(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m1")));
+ assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m2")));
+ assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m3")));
+ assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m4")));
+ assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m5")));
+ assertTrue(classPolicy.isMethodExposed(CAnnotationsTest2.class.getMethod("m6")));
+
+ assertTrue(classPolicy.isConstructorExposed(
+ CAnnotationsTest2.class.getConstructor()));
+ assertTrue(classPolicy.isConstructorExposed(
+ CAnnotationsTest2.class.getConstructor(int.class)));
+ assertTrue(classPolicy.isConstructorExposed(
+ CAnnotationsTest2.class.getConstructor(int.class, int.class)));
+ assertTrue(classPolicy.isConstructorExposed(
+ CAnnotationsTest2.class.getConstructor(int.class, int.class, int.class)));
+ assertFalse(classPolicy.isConstructorExposed(
+ CAnnotationsTest2.class.getConstructor(int.class, int.class, int.class, int.class)));
+ }
+
+ public static final MemberAccessPolicy CONFIG_TEST_MEMBER_ACCESS_POLICY =
+ newWhitelistMemberAccessPolicy(
+ C1.class.getName() + ".m1()",
+ C1.class.getName() + ".m3()");
+
+ @Test
+ public void stringBasedConfigurationTest() throws TemplateException {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setSetting(
+ "objectWrapper",
+ "DefaultObjectWrapper(2.3.30, " +
+ "memberAccessPolicy="
+ + WhitelistMemberAccessPolicyTest.class.getName() + ".CONFIG_TEST_MEMBER_ACCESS_POLICY"
+ + ")");
+ TemplateHashModel m = (TemplateHashModel) cfg.getObjectWrapper().wrap(new C1());
+ assertNotNull(m.get("m1"));
+ assertNull(m.get("m2"));
+ assertNotNull(m.get("m3"));
+ }
+
+ private static WhitelistMemberAccessPolicy newWhitelistMemberAccessPolicy(String... memberSelectors) {
+ return new WhitelistMemberAccessPolicy(
+ WhitelistMemberAccessPolicy.MemberSelector.parse(
+ Arrays.asList(memberSelectors),
+ WhitelistMemberAccessPolicyTest.class.getClassLoader()));
+ }
+
+ public static class C1 {
+ public int f1;
+ public int f2;
+
+ public C1() {
+ }
+
+ public C1(int x) {
+ }
+
+ public void m1() {
+ }
+
+ public void m2() {
+ }
+
+ public void m2(int x) {
+ }
+
+ public void m2(double x) {
+ }
+
+ public void m3() {
+ }
+ }
+
+ public static class C2 extends C1 {
+ public int f3;
+
+ public C2(int x) {
+ super(x);
+ }
+
+ public void m2(boolean x) {
+ }
+
+ public void m4() {
+ }
+ }
+
+ public static class C3 extends C2 {
+ public C3() {
+ super(0);
+ }
+
+ public C3(int x) {
+ super(x);
+ }
+ }
+
+ public static class D1 implements I1 {
+ public int f1;
+ public void m1() {
+ }
+ }
+
+ public static class D2 extends D1 {
+ }
+
+ public static class E1 implements I1Sub {
+ public void m1() {
+
+ }
+
+ public void m2() {
+ }
+ }
+
+ public static class E2 extends E1 implements J1 {
+ }
+
+ public static class F1 implements J1 {
+ public void m2() {
+ }
+ }
+
+ interface I1 {
+ int f1 = 1;
+ void m1();
+ }
+
+ interface I1Sub extends Serializable, I1 {
+ void m2();
+ }
+
+ interface J1 {
+ void m2();
+ }
+
+ public class CArrayArgs {
+ public void m1(String arg) {
+ }
+
+ public void m1(String[] arg) {
+ }
+
+ public void m1(String[][] arg) {
+ }
+
+ public void m2(C1[] arg) {
+ }
+
+ public void m2(C1[] arg1, C1[] arg2, C1 arg3) {
+ }
+ }
+
+ public static class CAnnotationsTest1 {
+ @TemplateAccessible
+ public int f5;
+
+ @TemplateAccessible
+ public CAnnotationsTest1() {}
+
+ @TemplateAccessible
+ public void m5() {}
+ }
+
+ public interface IAnnotationTest {
+ @TemplateAccessible
+ int f6 = 0;
+
+ @TemplateAccessible
+ void m6();
+ }
+
+ public static class CAnnotationsTest2 extends CAnnotationsTest1 implements IAnnotationTest {
+ public int f1;
+
+ public int f2;
+
+ @TemplateAccessible
+ public int f3;
+
+ @TemplateAccessible
+ public int f4;
+
+ public int f5;
+
+ public int f6;
+
+ public CAnnotationsTest2() {}
+
+ public CAnnotationsTest2(int x) {}
+
+ @TemplateAccessible
+ public CAnnotationsTest2(int x, int y) {}
+
+ @TemplateAccessible
+ public CAnnotationsTest2(int x, int y, int z) {}
+
+ public CAnnotationsTest2(int x, int y, int z, int a) {}
+
+ public void m1() {}
+
+ public void m2() {}
+
+ @TemplateAccessible
+ public void m3() {}
+
+ @TemplateAccessible
+ public void m4() {}
+
+ public void m5() {}
+
+ public void m6() {}
+ }
+
+}
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index 86d14bb..12f4954 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -27,6 +27,7 @@
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@@ -56,6 +57,7 @@
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.EnumerationModel;
import freemarker.ext.beans.HashAdapter;
+import freemarker.ext.beans.WhitelistMemberAccessPolicy;
import freemarker.ext.util.WrapperTemplateModel;
public class DefaultObjectWrapperTest {
@@ -316,6 +318,30 @@
assertTrue(bw.wrap(new PureIterable()) instanceof DefaultIterableAdapter);
}
+
+ {
+ DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());
+
+ DefaultObjectWrapper bwDefault = builder.build();
+ assertSame(bwDefault, builder.build());
+
+ WhitelistMemberAccessPolicy memberAccessPolicy =
+ new WhitelistMemberAccessPolicy(
+ WhitelistMemberAccessPolicy.MemberSelector.parse(
+ Arrays.asList(SomeBean.class.getName() + ".getX()"),
+ DefaultObjectWrapperTest.class.getClassLoader()));
+ builder.setMemberAccessPolicy(memberAccessPolicy);
+ DefaultObjectWrapper bw = builder.build();
+ assertNotSame(bw, bwDefault);
+ assertSame(bw, builder.build());
+ assertSame(bw.getMemberAccessPolicy(), memberAccessPolicy);
+
+ TemplateHashModel m = (TemplateHashModel) bw.wrap(new SomeBean());
+ assertNotNull(m.get("x"));
+ assertNotNull(m.get("getX"));
+ assertNull(m.get("y"));
+ assertNull(m.get("getY"));
+ }
}
@Test
@@ -1191,5 +1217,14 @@
}
};
+
+ public static class SomeBean {
+ public int getX() {
+ return 1;
+ }
+ public int getY() {
+ return 1;
+ }
+ }
}