This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git


The following commit(s) were added to refs/heads/3 by this push:
     new ca244aa  Forward ported from 2.3-gae: Added 
WhitelistMemberAccessPolicy and related internal classes
ca244aa is described below

commit ca244aa301fa39c6d9197584370fb63e9824ebf9
Author: ddekany <[email protected]>
AuthorDate: Wed Jan 1 00:41:40 2020 +0100

    Forward ported from 2.3-gae: Added WhitelistMemberAccessPolicy and related 
internal classes
---
 ...DefaultObjectWrapperMemberAccessPolicyTest.java | 120 ++++-
 .../core/model/impl/DefaultObjectWrapperTest.java  |  48 +-
 .../core/model/impl/MethodMatcherTest.java         | 179 +++++++
 .../freemarker/core/model/impl/MethodUtilTest.java | 156 ++++++
 .../impl/WhitelistMemberAccessPolicyTest.java      | 558 +++++++++++++++++++++
 .../core/model/impl/ClassIntrospector.java         |  71 +--
 ...erAccessPolicy.java => ConstructorMatcher.java} |  20 +-
 .../core/model/impl/DefaultObjectWrapper.java      |  20 +-
 .../core/model/impl/ExecutableMemberSignature.java |  67 +++
 .../{MemberAccessPolicy.java => FieldMatcher.java} |  20 +-
 .../core/model/impl/MemberAccessPolicy.java        |  39 +-
 .../freemarker/core/model/impl/MemberMatcher.java  | 109 ++++
 ...{MemberAccessPolicy.java => MethodMatcher.java} |  24 +-
 .../core/model/impl/TemplateAccessible.java        |  42 ++
 .../model/impl/WhitelistMemberAccessPolicy.java    | 408 +++++++++++++++
 .../freemarker/core/model/impl/_MethodUtils.java   | 141 ++++++
 .../apache/freemarker/core/util/_ClassUtils.java   |  33 +-
 17 files changed, 1955 insertions(+), 100 deletions(-)

diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
index 0343049..7d4826c 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -22,18 +22,25 @@ package org.apache.freemarker.core.model.impl;
 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.Map;
 
 import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Template;
 import org.apache.freemarker.core.TemplateException;
 import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper;
 import org.apache.freemarker.core.model.TemplateFunctionModel;
 import org.apache.freemarker.core.model.TemplateHashModel;
 import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
 import org.junit.Test;
 
+import com.google.common.collect.ImmutableMap;
+
 public class DefaultObjectWrapperMemberAccessPolicyTest {
 
     @Test
@@ -160,7 +167,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
     public void testMethodsWithCustomMemberAccessPolicy() throws 
TemplateException {
         DefaultObjectWrapper.Builder owb = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
         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();
@@ -202,7 +209,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
         DefaultObjectWrapper.Builder owb = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
         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;
@@ -232,7 +239,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
     public void testGenericGetWithCustomMemberAccessPolicy() throws 
TemplateException {
         DefaultObjectWrapper.Builder owb = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
         owb.setMemberAccessPolicy(new MemberAccessPolicy() {
-            public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
                 return new ClassMemberAccessPolicy() {
                     public boolean isMethodExposed(Method method) {
                         return false;
@@ -258,7 +265,7 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
     public void testConstructorsWithCustomMemberAccessPolicy() throws 
TemplateException {
         DefaultObjectWrapper.Builder owb = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0);
         owb.setMemberAccessPolicy(new MemberAccessPolicy() {
-            public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+            public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
                 return new ClassMemberAccessPolicy() {
                     public boolean isMethodExposed(Method method) {
                         return true;
@@ -299,6 +306,100 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
                         new TemplateModel[] {new SimpleNumber(1)}, 
null).getClass());
     }
 
+    @Test
+    public void testMemberAccessPolicyAndApiBI() throws IOException, 
TemplateException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .memberAccessPolicy(new MemberAccessPolicy() {
+                    @Override
+                    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;
+                            }
+                        };
+                    }
+                })
+                .build();
+
+        Map<String, Object> dataModel = ImmutableMap.<String, Object>of("m", 
ImmutableMap.of("k", "v"));
+
+        String templateSource = "size=${m?api.size()} 
get=${(m?api.get('k'))!'hidden'}";
+
+        {
+            Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0)
+                    .objectWrapper(ow)
+                    .apiBuiltinEnabled(true)
+                    .build();
+            Template template = new Template(null, templateSource, cfg);
+            StringWriter out = new StringWriter();
+            template.process(dataModel, out);
+            assertEquals("size=1 get=hidden", out.toString());
+        }
+
+        {
+            Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0)
+                    .objectWrapper(new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build())
+                    .apiBuiltinEnabled(true)
+                    .build();
+            Template template = new Template(null, templateSource, cfg);
+            StringWriter out = new StringWriter();
+            template.process(dataModel, out);
+            assertEquals("size=1 get=v", out.toString());
+        }
+    }
+
+    @Test
+    public void testMemberAccessPolicyAndNewBI() throws IOException, 
TemplateException {
+        DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                .memberAccessPolicy(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;
+                                }
+                            };
+                        }
+                    })
+                .build();
+
+        String templateSource = "${'" + CustomModel.class.getName() + 
"'?new()} "
+                + "<#attempt>${'" + OtherCustomModel.class.getName() + 
"'?new()}<#recover>failed</#attempt>";
+        {
+            Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0)
+                    .objectWrapper(ow)
+                    .apiBuiltinEnabled(true)
+                    .build();
+            Template template = new Template(null, templateSource, cfg);
+            StringWriter out = new StringWriter();
+            template.process(null, out);
+            assertEquals("1 failed", out.toString());
+        }
+
+        {
+            Configuration cfg = new 
Configuration.Builder(Configuration.VERSION_3_0_0).build();
+            Template template = new Template(null, templateSource, cfg);
+            StringWriter out = new StringWriter();
+            template.process(null, out);
+            assertEquals("1 2", out.toString());
+        }
+    }
+
     private static DefaultObjectWrapper 
createDefaultMemberAccessPolicyObjectWrapper() {
         return new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
     }
@@ -400,4 +501,15 @@ public class DefaultObjectWrapperMemberAccessPolicyTest {
         }
     }
 
+    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/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
index 97a46f4..9a1dddb 100644
--- 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapperTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.*;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -131,10 +132,40 @@ public class DefaultObjectWrapperTest {
     @SuppressWarnings("boxing")
     @Test
     public void testBuilder() throws Exception {
-        DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
-        assertSame(ow, new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
-        assertSame(ow.getClass(), DefaultObjectWrapper.class);
-        assertEquals(Configuration.VERSION_3_0_0, 
ow.getIncompatibleImprovements());
+        {
+            DefaultObjectWrapper ow = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertSame(ow, new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+            assertSame(ow.getClass(), DefaultObjectWrapper.class);
+            assertEquals(Configuration.VERSION_3_0_0, 
ow.getIncompatibleImprovements());
+        }
+
+        {
+            DefaultObjectWrapper bwDefault = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+            assertSame(bwDefault, new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).build());
+
+            WhitelistMemberAccessPolicy memberAccessPolicy =
+                    new WhitelistMemberAccessPolicy(
+                            WhitelistMemberAccessPolicy.MemberSelector.parse(
+                                    Arrays.asList(SomeBean.class.getName() + 
".getX()"),
+                                    
DefaultObjectWrapperTest.class.getClassLoader()));
+
+            DefaultObjectWrapper bw = new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                    .memberAccessPolicy(memberAccessPolicy)
+                    .build();
+            assertNotSame(bw, bwDefault);
+            assertSame(
+                    bw,
+                    new 
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+                            .memberAccessPolicy(memberAccessPolicy)
+                            .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
@@ -900,5 +931,14 @@ public class DefaultObjectWrapperTest {
         }
 
     }
+
+    public static class SomeBean {
+        public int getX() {
+            return 1;
+        }
+        public int getY() {
+            return 1;
+        }
+    }
     
 }
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MethodMatcherTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MethodMatcherTest.java
new file mode 100644
index 0000000..effeb13
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/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 org.apache.freemarker.core.model.impl;
+
+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) {
+        }
+    }
+
+}
\ No newline at end of file
diff --git 
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MethodUtilTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MethodUtilTest.java
new file mode 100644
index 0000000..05f7b93
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/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 org.apache.freemarker.core.model.impl;
+
+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(_MethodUtils.getInheritableAnnotation(
+                C1.class, C1.class.getMethod("m1"), TemplateAccessible.class));
+        assertNull(_MethodUtils.getInheritableAnnotation(
+                C1.class, C1.class.getMethod("m2"), TemplateAccessible.class));
+
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C1.class, C1.class.getConstructor(int.class), 
TemplateAccessible.class));
+        assertNull(_MethodUtils.getInheritableAnnotation(
+                C1.class, C1.class.getConstructor(int.class, int.class), 
TemplateAccessible.class));
+
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C1.class, C1.class.getField("f1"), TemplateAccessible.class));
+        assertNull(_MethodUtils.getInheritableAnnotation(
+                C1.class, C1.class.getField("f3"), TemplateAccessible.class));
+    }
+
+    @Test
+    public void testMethodInheritance() throws NoSuchMethodException, 
NoSuchFieldException {
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m1"), TemplateAccessible.class));
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m2"), TemplateAccessible.class));
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m3"), TemplateAccessible.class));
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m4"), TemplateAccessible.class));
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getMethod("m5"), TemplateAccessible.class));
+
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getConstructor(int.class), 
TemplateAccessible.class));
+        assertNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getConstructor(), 
TemplateAccessible.class));
+
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getField("f1"), TemplateAccessible.class));
+        assertNotNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getField("f2"), TemplateAccessible.class));
+        assertNull(_MethodUtils.getInheritableAnnotation(
+                C2.class, C2.class.getField("f3"), TemplateAccessible.class));
+        assertNotNull(_MethodUtils.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(_MethodUtils.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/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicyTest.java
 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicyTest.java
new file mode 100644
index 0000000..9ed20be
--- /dev/null
+++ 
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/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 org.apache.freemarker.core.model.impl;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.junit.Test;
+
+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.Builder(Configuration.VERSION_3_0_0)
+                .setting(
+                        "objectWrapper",
+                        "DefaultObjectWrapper(3.0.0, " +
+                                "memberAccessPolicy="
+                                + 
WhitelistMemberAccessPolicyTest.class.getName() + 
".CONFIG_TEST_MEMBER_ACCESS_POLICY"
+                                + ")")
+                .build();
+        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/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
index b0676d6..e48c658 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassIntrospector.java
@@ -78,6 +78,11 @@ class ClassIntrospector {
     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 });
+
     private static final ClassChangeNotifier CLASS_CHANGE_NOTIFIER;
     static {
         boolean jRebelAvailable;
@@ -257,7 +262,7 @@ class ClassIntrospector {
             addFieldsToClassIntrospectionData(introspData, clazz, 
classMemberAccessPolicy);
         }
 
-        final Map<MethodSignature, List<Method>> accessibleMethods = 
discoverAccessibleMethods(clazz);
+        final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = 
discoverAccessibleMethods(clazz);
 
         addGenericGetToClassIntrospectionData(introspData, accessibleMethods, 
classMemberAccessPolicy);
 
@@ -294,7 +299,7 @@ class ClassIntrospector {
     }
 
     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);
@@ -641,7 +646,7 @@ class ClassIntrospector {
     }
 
     private void addPropertyDescriptorToClassIntrospectionData(Map<Object, 
Object> introspData,
-            PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, 
List<Method>> accessibleMethods,
+            PropertyDescriptor pd, Class<?> clazz, 
Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
             ClassMemberAccessPolicy classMemberAccessPolicy) {
         Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), 
accessibleMethods);
         if (readMethod != null && isMethodExposed(classMemberAccessPolicy, 
readMethod)) {
@@ -650,12 +655,12 @@ class ClassIntrospector {
     }
 
     private void addGenericGetToClassIntrospectionData(Map<Object, Object> 
introspData,
-            Map<MethodSignature, List<Method>> accessibleMethods, 
ClassMemberAccessPolicy classMemberAccessPolicy) {
+            Map<ExecutableMemberSignature, List<Method>> accessibleMethods, 
ClassMemberAccessPolicy classMemberAccessPolicy) {
         Method genericGet = getFirstAccessibleMethod(
-                MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+                GET_STRING_SIGNATURE, accessibleMethods);
         if (genericGet == null) {
             genericGet = getFirstAccessibleMethod(
-                    MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+                    GET_OBJECT_SIGNATURE, accessibleMethods);
         }
         if (genericGet != null && isMethodExposed(classMemberAccessPolicy, 
genericGet)) {
             introspData.put(GENERIC_GET_KEY, genericGet);
@@ -693,22 +698,23 @@ class ClassIntrospector {
     }
 
     /**
-     * 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<>();
+    private static Map<ExecutableMemberSignature, List<Method>> 
discoverAccessibleMethods(Class<?> clazz) {
+        Map<ExecutableMemberSignature, List<Method>> accessibles = new 
HashMap<>();
         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 (Method method : methods) {
-                    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
@@ -746,11 +752,11 @@ class ClassIntrospector {
         }
     }
 
-    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;
@@ -763,7 +769,7 @@ class ClassIntrospector {
         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;
@@ -810,39 +816,6 @@ class ClassIntrospector {
         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/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ConstructorMatcher.java
similarity index 53%
copy from 
freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
copy to 
freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ConstructorMatcher.java
index 27be4f0..0300fd4 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ConstructorMatcher.java
@@ -19,16 +19,14 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.reflect.Constructor;
+
 /**
- * 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).
+ * {@link MemberMatcher} for constructors.
  */
-public interface MemberAccessPolicy {
-    /**
-     * Returns the {@link ClassMemberAccessPolicy} that encapsulates the 
member access policy for a given class.
-     */
-    ClassMemberAccessPolicy forClass(Class<?> containingClass);
-}
\ No newline at end of file
+final class ConstructorMatcher extends MemberMatcher<Constructor<?>, 
ExecutableMemberSignature> {
+    @Override
+    protected ExecutableMemberSignature toMemberSignature(Constructor<?> 
member) {
+        return new ExecutableMemberSignature(member);
+    }
+}
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
index 7a9fe04..96a8655 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
@@ -282,6 +282,10 @@ public class DefaultObjectWrapper implements 
RichObjectWrapper {
         return classIntrospector.getMethodAppearanceFineTuner();
     }
 
+    public MemberAccessPolicy getMemberAccessPolicy() {
+        return classIntrospector.getMemberAccessPolicy();
+    }
+
     MethodSorter getMethodSorter() {
         return classIntrospector.getMethodSorter();
     }
@@ -1042,7 +1046,7 @@ public class DefaultObjectWrapper implements 
RichObjectWrapper {
         try {
             Object ctors = 
classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY);
             if (ctors == null) {
-                throw new TemplateException("Class " + clazz.getName() + " has 
no public constructors.");
+                throw new TemplateException("Class " + clazz.getName() + " has 
no exposed constructors.");
             }
             Constructor<?> ctor = null;
             Object[] pojoArgs;
@@ -1807,14 +1811,26 @@ public class DefaultObjectWrapper implements 
RichObjectWrapper {
             return classIntrospectorBuilder.getMemberAccessPolicy();
         }
 
+        /**
+         * Sets the {@link MemberAccessPolicy}; default is {@link 
DefaultMemberAccessPolicy#getInstance(Version)}, which
+         * is not appropriate if template editors aren't trusted.
+         */
         public void setMemberAccessPolicy(MemberAccessPolicy 
memberAccessPolicy) {
             classIntrospectorBuilder.setMemberAccessPolicy(memberAccessPolicy);
         }
 
         /**
+         * Fluent API equivalent of {@link 
#setMemberAccessPolicy(MemberAccessPolicy)}
+         */
+        public SelfT memberAccessPolicy(MemberAccessPolicy memberAccessPolicy) 
{
+            setMemberAccessPolicy(memberAccessPolicy);
+            return self();
+        }
+
+        /**
          * Tells if the property was explicitly set, as opposed to just 
holding its default value.
          */
-        public boolean isMemberAccessPolicy() {
+        public boolean isMemberAccessPolicySet() {
             return classIntrospectorBuilder.isMemberAccessPolicySet();
         }
 
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ExecutableMemberSignature.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ExecutableMemberSignature.java
new file mode 100644
index 0000000..db63d31
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ExecutableMemberSignature.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.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.
+ */
+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/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/FieldMatcher.java
similarity index 53%
copy from 
freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
copy to 
freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/FieldMatcher.java
index 27be4f0..dda187f 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/FieldMatcher.java
@@ -19,16 +19,14 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.reflect.Field;
+
 /**
- * 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).
+ * {@link MemberMatcher} for fields.
  */
-public interface MemberAccessPolicy {
-    /**
-     * Returns the {@link ClassMemberAccessPolicy} that encapsulates the 
member access policy for a given class.
-     */
-    ClassMemberAccessPolicy forClass(Class<?> containingClass);
-}
\ No newline at end of file
+final class FieldMatcher extends MemberMatcher<Field, String> {
+    @Override
+    protected String toMemberSignature(Field member) {
+        return member.getName();
+    }
+}
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
index 27be4f0..c5fa8b6 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
@@ -19,16 +19,41 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.model.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 
DefaultObjectWrapper.Builder#setMemberAccessPolicy(MemberAccessPolicy)} (or if
+ * you use {@link DefaultObjectWrapper}, with
+ * {@link 
DefaultObjectWrapper.Builder#setMemberAccessPolicy(MemberAccessPolicy)}).
+ *
+ * <p>As {@link DefaultObjectWrapper}, 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 DefaultObjectWrapper} intends to expose on 
the first place. (Also, while public members
+ * declared in non-public classes are discovered by {@link 
DefaultObjectWrapper}, 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.
  */
 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);
-}
\ No newline at end of file
+    ClassMemberAccessPolicy forClass(Class<?> contextClass);
+}
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberMatcher.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberMatcher.java
new file mode 100644
index 0000000..76080a1
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberMatcher.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.reflect.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.
+ */
+abstract class MemberMatcher<M extends Member, S> {
+    private final Map<S, Types> signaturesToUpperBoundTypes = new HashMap<>();
+
+    private static class Types {
+        private final Set<Class<?>> set = new HashSet<>();
+        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/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodMatcher.java
similarity index 53%
copy from 
freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
copy to 
freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodMatcher.java
index 27be4f0..ce66f4c 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MethodMatcher.java
@@ -19,16 +19,18 @@
 
 package org.apache.freemarker.core.model.impl;
 
+import java.lang.reflect.Method;
+
 /**
- * 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).
+ * {@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 
DefaultObjectWrapper} itself will still filter by
+ * visibility, it's just not the duty of the {@link MemberMatcher}.)
  */
-public interface MemberAccessPolicy {
-    /**
-     * Returns the {@link ClassMemberAccessPolicy} that encapsulates the 
member access policy for a given class.
-     */
-    ClassMemberAccessPolicy forClass(Class<?> containingClass);
-}
\ No newline at end of file
+final class MethodMatcher extends MemberMatcher<Method, 
ExecutableMemberSignature> {
+    @Override
+    protected ExecutableMemberSignature toMemberSignature(Method member) {
+        return new ExecutableMemberSignature(member);
+    }
+}
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateAccessible.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateAccessible.java
new file mode 100644
index 0000000..d92d0f6
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/TemplateAccessible.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.freemarker.core.model.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 DefaultObjectWrapper} 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.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+public @interface TemplateAccessible {
+}
diff --git 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
new file mode 100644
index 0000000..64ef7a9
--- /dev/null
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/WhitelistMemberAccessPolicy.java
@@ -0,0 +1,408 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.apache.freemarker.core.model.ObjectWrapper;
+import org.apache.freemarker.core.util._ClassUtils;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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.
+ */
+public class WhitelistMemberAccessPolicy implements MemberAccessPolicy {
+    private static final Logger LOG = 
LoggerFactory.getLogger(WhitelistMemberAccessPolicy.class);
+
+    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 = 
_ClassUtils.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 | SecurityException e) 
{
+                            return new MemberSelector(upperBoundClass, e, 
cleanedStr);
+                        }
+                    }
+                    argTypes[i] = _ClassUtils.getArrayClass(argClass, 
arrayDimensions);
+                }
+                try {
+                    return memberName.equals(upperBoundClass.getSimpleName())
+                            ? new MemberSelector(upperBoundClass, 
upperBoundClass.getConstructor(argTypes))
+                            : new MemberSelector(upperBoundClass, 
upperBoundClass.getMethod(memberName, argTypes));
+                } catch (NoSuchMethodException | SecurityException e) {
+                    return new MemberSelector(upperBoundClass, e, cleanedStr);
+                }
+            } else {
+                try {
+                    return new MemberSelector(upperBoundClass, 
upperBoundClass.getField(memberName));
+                } catch (NoSuchFieldException | 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<>(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();
+            }
+        }
+    }
+
+    @Override
+    public ClassMemberAccessPolicy forClass(final Class<?> contextClass) {
+        return new ClassMemberAccessPolicy() {
+            @Override
+            public boolean isMethodExposed(Method method) {
+                return methodMatcher.matches(contextClass, method)
+                        || _MethodUtils.getInheritableAnnotation(contextClass, 
method, TemplateAccessible.class) != null;
+            }
+
+            @Override
+            public boolean isConstructorExposed(Constructor<?> constructor) {
+                return constructorMatcher.matches(contextClass, constructor)
+                        || _MethodUtils.getInheritableAnnotation(contextClass, 
constructor, TemplateAccessible.class)
+                        != null;
+            }
+
+            @Override
+            public boolean isFieldExposed(Field field) {
+                return fieldMatcher.matches(contextClass, field)
+                        || _MethodUtils.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/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtils.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtils.java
index 1deaa95..5b42d20 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtils.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/_MethodUtils.java
@@ -18,7 +18,9 @@
  */
 package org.apache.freemarker.core.model.impl;
 
+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;
@@ -315,4 +317,143 @@ public final class _MethodUtils {
                 .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/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtils.java
 
b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtils.java
index 21e62c0..43bf1ac 100644
--- 
a/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtils.java
+++ 
b/freemarker-core/src/main/java/org/apache/freemarker/core/util/_ClassUtils.java
@@ -21,7 +21,10 @@ package org.apache.freemarker.core.util;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.reflect.Array;
 import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Properties;
 
 import org.apache.freemarker.core.model.impl.BeanModel;
@@ -58,7 +61,35 @@ public class _ClassUtils {
         // 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<>();
+        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.
+     */
+    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.
+     */
+    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)}.
      */

Reply via email to