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)}.
*/