Merge pull request #64 from tocco/fix_extended_decimal_format_parser
FREEMARKER-125: ExtendedDecimalFormatParser now picks up DecimalFormatSymbols provided by DecimalFormatSymbolsProvider SPI.
diff --git a/README.md b/README.md
index dab9870..d8197cb 100644
--- a/README.md
+++ b/README.md
@@ -83,7 +83,7 @@
included in OpenJDK anymore. It's not needed on Oracle Java 9,
or if FreeMarker is configured to use Jaxen for XPath.
-The minimum required Java version is currently Java SE 5. (The presence
+The minimum required Java version is currently Java SE 7. (The presence
of a later version may be detected on runtime and utilized by
FreeMarker.)
@@ -201,7 +201,7 @@
- Press "Finish"
- Eclipse will indicate many errors at this point; it's expected, read on.
- Project -> Properties -> Java Compiler
- - Set "Compiler Compliance Level" to "1.5" (you will have to uncheck
+ - Set "Compiler Compliance Level" to "1.7" (you will have to uncheck
"Use compliance from execution environment" for that)
- In Errors/Warnings, check in "Enable project specific settings", then set
"Forbidden reference (access rules)" from "Error" to "Warning".
@@ -272,8 +272,8 @@
- Test Resource Folders:
src/test/resources
- - Still inside the "Sources" tab, change the "Language level" to "5". (Yes, we use Java 8 SDK with
- language level 5 in the IDE, due to the tricks FreeMarker uses to support different Java versions.)
+ - Still inside the "Sources" tab, change the "Language level" to "7". (Yes, we use Java 8 SDK with
+ language level 7 in the IDE, due to the tricks FreeMarker uses to support different Java versions.)
- Switch over to the "Dependencies" tab (still inside "Project Structure" / "Modules"), and add
all the jar-s inside the `ide-dependencies` directory as dependency. (How: Click the "+" icon
diff --git a/build.properties.sample b/build.properties.sample
index 56315b3..d5e31cc 100644
--- a/build.properties.sample
+++ b/build.properties.sample
@@ -17,8 +17,7 @@
# Copy this file to "build.properties" before editing!
# These propeties should point to the rt.jar-s of the respective J2SE versions:
-boot.classpath.j2se1.5=C:/Program Files (x86)/Java/jdk1.5.0_16/jre/lib/rt.jar
-boot.classpath.j2se1.6=C:/Program Files/Java/jdk1.6.0_24/jre/lib/rt.jar
+boot.classpath.j2se1.7=c:/Program Files/Java/jre7/lib/rt.jar
boot.classpath.j2se1.8=C:/Program Files/Java/jdk1.8.0_66/jre/lib/rt.jar
mvnCommand=C:/Program Files (x86)/maven3/bin/mvn.bat
gpgCommand=C:/Program Files (x86)/GNU/GnuPG/pub/gpg.exe
\ No newline at end of file
diff --git a/build.xml b/build.xml
index 1953b75..5098e11 100644
--- a/build.xml
+++ b/build.xml
@@ -43,11 +43,8 @@
<property name="server.ivy.repo.root" value="${basedir}/build/dummy-server-ivy-repo" />
<property file="build.properties"/>
- <condition property="has.explicit.boot.classpath.j2se1.5">
- <isset property="boot.classpath.j2se1.5"/>
- </condition>
- <condition property="has.explicit.boot.classpath.j2se1.6">
- <isset property="boot.classpath.j2se1.6"/>
+ <condition property="has.explicit.boot.classpath.j2se1.7">
+ <isset property="boot.classpath.j2se1.7"/>
</condition>
<condition property="has.explicit.boot.classpath.j2se1.8">
<isset property="boot.classpath.j2se1.8"/>
@@ -61,18 +58,13 @@
<!-- When boot.classpath.j2se* is missing, these will be the defaults: -->
<!-- Note: Target "dist" doesn't allow using these. -->
- <property name="boot.classpath.j2se1.5" value="${sun.boot.class.path}" />
- <property name="boot.classpath.j2se1.6" value="${sun.boot.class.path}" />
+ <property name="boot.classpath.j2se1.7" value="${sun.boot.class.path}" />
<property name="boot.classpath.j2se1.8" value="${sun.boot.class.path}" />
<!-- For checking the correctness of the boot.classpath.j2se* -->
- <available classpath="${boot.classpath.j2se1.5}"
- classname="java.util.concurrent.ConcurrentHashMap" ignoresystemclasses="true"
- property="boot.classpath.j2se1.5.correct"
- />
- <available classpath="${boot.classpath.j2se1.6}"
- classname="java.util.ServiceLoader" ignoresystemclasses="true"
- property="boot.classpath.j2se1.6.correct"
+ <available classpath="${boot.classpath.j2se1.7}"
+ classname="java.nio.file.Path" ignoresystemclasses="true"
+ property="boot.classpath.j2se1.7.correct"
/>
<available classpath="${boot.classpath.j2se1.8}"
classname="java.time.Instant" ignoresystemclasses="true"
@@ -210,14 +202,8 @@
</target>
<target name="compile" depends="javacc">
- <fail unless="boot.classpath.j2se1.5.correct"><!--
- -->The "boot.classpath.j2se1.5" property value (${boot.classpath.j2se1.5}) <!--
- -->seems to be an incorrect boot classpath. Please fix it in <!--
- -->the <projectDir>/build.properties file, or wherever you <!--
- -->set it.<!--
- --></fail>
- <fail unless="boot.classpath.j2se1.6.correct"><!--
- -->The "boot.classpath.j2se1.6" property value (${boot.classpath.j2se1.6}) <!--
+ <fail unless="boot.classpath.j2se1.7.correct"><!--
+ -->The "boot.classpath.j2se1.7" property value (${boot.classpath.j2se1.7}) <!--
-->seems to be an incorrect boot classpath. Please fix it in <!--
-->the <projectDir>/build.properties file, or wherever you <!--
-->set it.<!--
@@ -229,9 +215,8 @@
-->set it.<!--
--></fail>
<echo level="info"><!--
- -->Using boot classpaths:<!--
- -->Java 5: ${boot.classpath.j2se1.5}; <!--
- -->Java 6: ${boot.classpath.j2se1.6}<!--
+ -->Using boot classpaths: <!--
+ -->Java 7: ${boot.classpath.j2se1.7};<!--
-->Java 8: ${boot.classpath.j2se1.8}<!--
--></echo>
@@ -259,10 +244,10 @@
<!-- Note: the "build.base" conf doesn't include optional FreeMarker dependencies. -->
<ivy:cachepath conf="build.base" pathid="ivy.dep" />
<javac destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+ debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep"
- bootclasspath="${boot.classpath.j2se1.5}"
+ bootclasspath="${boot.classpath.j2se1.7}"
excludes="
freemarker/core/_Java?*Impl.java,
freemarker/ext/jsp/**,
@@ -281,15 +266,6 @@
<ivy:cachepath conf="build.base" pathid="ivy.dep" />
<javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.6" source="1.6" encoding="utf-8"
- includeantruntime="false"
- classpathref="ivy.dep"
- bootclasspath="${boot.classpath.j2se1.6}"
- includes="freemarker/core/_Java6Impl.java"
- />
-
- <ivy:cachepath conf="build.base" pathid="ivy.dep" />
- <javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
debug="on" optimize="off" target="1.8" source="1.8" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep"
@@ -305,10 +281,10 @@
<ivy:cachepath conf="build.jsp2.0" pathid="ivy.dep.jsp2.0" />
<javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+ debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep.jsp2.0"
- bootclasspath="${boot.classpath.j2se1.5}"
+ bootclasspath="${boot.classpath.j2se1.7}"
includes="
freemarker/ext/jsp/**,
freemarker/ext/servlet/**,
@@ -323,10 +299,10 @@
<ivy:cachepath conf="build.jsp2.1" pathid="ivy.dep.jsp2.1" />
<javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+ debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep.jsp2.1"
- bootclasspath="${boot.classpath.j2se1.5}"
+ bootclasspath="${boot.classpath.j2se1.7}"
includes="
freemarker/ext/jsp/_FreeMarkerPageContext21.java,
freemarker/ext/jsp/FreeMarkerJspFactory21.java,
@@ -335,10 +311,10 @@
<ivy:cachepath conf="build.jython2.0" pathid="ivy.dep.jython2.0" />
<javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+ debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep.jython2.0"
- bootclasspath="${boot.classpath.j2se1.5}"
+ bootclasspath="${boot.classpath.j2se1.7}"
includes="
freemarker/ext/ant/**,
freemarker/template/utility/JythonRuntime.java,
@@ -350,20 +326,20 @@
<ivy:cachepath conf="build.jython2.2" pathid="ivy.dep.jython2.2" />
<javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+ debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep.jython2.2"
- bootclasspath="${boot.classpath.j2se1.5}"
+ bootclasspath="${boot.classpath.j2se1.7}"
includes="
freemarker/ext/jython/_Jython22VersionAdapter.java"
/>
<ivy:cachepath conf="build.jython2.5" pathid="ivy.dep.jython2.5" />
<javac srcdir="build/src-main-java-filtered" destdir="build/classes" deprecation="off"
- debug="on" optimize="off" target="1.5" source="1.5" encoding="utf-8"
+ debug="on" optimize="off" target="1.7" source="1.7" encoding="utf-8"
includeantruntime="false"
classpathref="ivy.dep.jython2.5"
- bootclasspath="${boot.classpath.j2se1.5}"
+ bootclasspath="${boot.classpath.j2se1.7}"
includes="
freemarker/ext/jython/_Jython25VersionAdapter.java"
/>
diff --git a/src/main/java/freemarker/core/AddConcatExpression.java b/src/main/java/freemarker/core/AddConcatExpression.java
index 40d0bb6..b89b472 100644
--- a/src/main/java/freemarker/core/AddConcatExpression.java
+++ b/src/main/java/freemarker/core/AddConcatExpression.java
@@ -35,6 +35,7 @@
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
/**
* An operator for the + operator. Note that this is treated
@@ -266,7 +267,7 @@
throws TemplateModelException {
if (keys == null) {
HashSet keySet = new HashSet();
- SimpleSequence keySeq = new SimpleSequence(32);
+ SimpleSequence keySeq = new SimpleSequence(32, _TemplateAPI.SAFE_OBJECT_WRAPPER);
addKeys(keySet, keySeq, (TemplateHashModelEx) this.left);
addKeys(keySet, keySeq, (TemplateHashModelEx) this.right);
keys = new CollectionAndSequence(keySeq);
@@ -289,7 +290,7 @@
private void initValues()
throws TemplateModelException {
if (values == null) {
- SimpleSequence seq = new SimpleSequence(size());
+ SimpleSequence seq = new SimpleSequence(size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
// Note: size() invokes initKeys() if needed.
int ln = keys.size();
diff --git a/src/main/java/freemarker/core/BuiltInsForNodes.java b/src/main/java/freemarker/core/BuiltInsForNodes.java
index efeae9b..acd9fdf 100644
--- a/src/main/java/freemarker/core/BuiltInsForNodes.java
+++ b/src/main/java/freemarker/core/BuiltInsForNodes.java
@@ -30,8 +30,9 @@
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNodeModel;
import freemarker.template.TemplateNodeModelEx;
+import freemarker.template._TemplateAPI;
-/**
+ /**
* A holder for builtins that operate exclusively on (XML-)node left-hand value.
*/
class BuiltInsForNodes {
@@ -122,6 +123,7 @@
private Environment env;
AncestorSequence(Environment env) {
+ super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
this.env = env;
}
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java
index 6d76e1d..fcaeebd 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -45,6 +45,7 @@
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.Constants;
import freemarker.template.utility.StringUtil;
@@ -805,7 +806,7 @@
}
}
- // Sort tje List[KVP]:
+ // Sort the List[KVP]:
try {
Collections.sort(res, keyComparator);
} catch (Exception exc) {
@@ -870,8 +871,10 @@
if (!lazilyGeneratedResultEnabled) {
SimpleSequence seq =
coll instanceof TemplateCollectionModelEx
- ? new SimpleSequence(((TemplateCollectionModelEx) coll).size())
- : new SimpleSequence();
+ ? new SimpleSequence(
+ ((TemplateCollectionModelEx) coll).size(),
+ _TemplateAPI.SAFE_OBJECT_WRAPPER)
+ : new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) {
seq.add(iter.next());
}
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
index 7583984..4afffb8 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
@@ -35,6 +35,7 @@
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.StringUtil;
class BuiltInsForStringsBasic {
@@ -804,7 +805,7 @@
static class word_listBI extends BuiltInForString {
@Override
TemplateModel calculateResult(String s, Environment env) {
- SimpleSequence result = new SimpleSequence();
+ SimpleSequence result = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
StringTokenizer st = new StringTokenizer(s);
while (st.hasMoreTokens()) {
result.add(st.nextToken());
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java b/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
index 584833d..d6d3082 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsRegexp.java
@@ -35,6 +35,7 @@
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.StringUtil;
@@ -138,11 +139,11 @@
static class MatchWithGroups implements TemplateScalarModel {
final String matchedInputPart;
final SimpleSequence groupsSeq;
-
+
MatchWithGroups(String input, Matcher matcher) {
matchedInputPart = input.substring(matcher.start(), matcher.end());
final int grpCount = matcher.groupCount() + 1;
- groupsSeq = new SimpleSequence(grpCount);
+ groupsSeq = new SimpleSequence(grpCount, _TemplateAPI.SAFE_OBJECT_WRAPPER);
for (int i = 0; i < grpCount; i++) {
groupsSeq.add(matcher.group(i));
}
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 89f3445..a0ad656 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -52,6 +52,7 @@
import freemarker.cache.TemplateLoader;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
+import freemarker.ext.beans.MemberAccessPolicy;
import freemarker.template.AttemptExceptionReporter;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
@@ -1565,6 +1566,10 @@
* FreeMarker 2.3.x, and {@link TemplateClassResolver#SAFER_RESOLVER}
* starting from FreeMarker 2.4.0. If you allow users to upload templates,
* it's important to use a custom restrictive {@link TemplateClassResolver}.
+ *
+ * <p>Note that the {@link MemberAccessPolicy} used by the {@link ObjectWrapper} also influences what constructors
+ * are available. Allowing the resolution of the class here is not enough in itself, as the
+ * {@link MemberAccessPolicy} has to allow exposing the particular constructor you try to call as well.
*
* @since 2.3.17
*/
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index 4a72543..e455b94 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -21,7 +21,6 @@
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import freemarker.template.SimpleScalar;
@@ -291,8 +290,8 @@
resultList.add(targetSeq.get(srcIdx));
srcIdx += step;
}
- // List items are already wrapped, so the wrapper will be null:
- return new SimpleSequence(resultList, null);
+ // List items are already wrapped:
+ return new SimpleSequence(resultList, _TemplateAPI.SAFE_OBJECT_WRAPPER);
} else if (targetLazySeq != null) {
// As a targetLazySeq can only occur if a new built-in like ?filter or ?map was used somewhere in the target
// expression, in this case we can return lazily generated sequence without breaking backward compatibility.
@@ -384,8 +383,8 @@
}
resultList.add(targetIter.next());
}
- // List items are already wrapped, so the wrapper will be null:
- return new SimpleSequence(resultList, null);
+ // List items are already wrapped:
+ return new SimpleSequence(resultList, _TemplateAPI.SAFE_OBJECT_WRAPPER);
}
}
@@ -432,13 +431,13 @@
"Range top index " + highIndex + " (0-based) is outside the sliced sequence of length " +
srcIdx + ".");
}
- return new SimpleSequence(Arrays.asList(resultElements), null);
+ return new SimpleSequence(Arrays.asList(resultElements), _TemplateAPI.SAFE_OBJECT_WRAPPER);
}
private TemplateModel emptyResult(boolean seq) {
return seq
? (_TemplateAPI.getTemplateLanguageVersionAsInt(this) < _TemplateAPI.VERSION_INT_2_3_21
- ? new SimpleSequence(Collections.EMPTY_LIST, null)
+ ? new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER)
: Constants.EMPTY_SEQUENCE)
: TemplateScalarModel.EMPTY_STRING;
}
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index fcce497..c48954e 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -725,7 +725,7 @@
void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel namespaces)
throws TemplateException, IOException {
if (nodeNamespaces == null) {
- SimpleSequence ss = new SimpleSequence(1);
+ SimpleSequence ss = new SimpleSequence(1, _TemplateAPI.SAFE_OBJECT_WRAPPER);
ss.add(currentNamespace);
nodeNamespaces = ss;
}
@@ -1122,14 +1122,15 @@
private static SimpleSequence initPositionalCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) {
SimpleSequence positionalCatchAllParamValue;
- positionalCatchAllParamValue = new SimpleSequence((ObjectWrapper) null);
+ positionalCatchAllParamValue = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
macroCtx.setLocalVar(catchAllParamName, positionalCatchAllParamValue);
return positionalCatchAllParamValue;
}
private static SimpleHash initNamedCatchAllParameter(Macro.Context macroCtx, String catchAllParamName) {
SimpleHash namedCatchAllParamValue;
- namedCatchAllParamValue = new SimpleHash(new LinkedHashMap<String, Object>(), null, 0);
+ namedCatchAllParamValue = new SimpleHash(
+ new LinkedHashMap<String, Object>(), _TemplateAPI.SAFE_OBJECT_WRAPPER, 0);
macroCtx.setLocalVar(catchAllParamName, namedCatchAllParamValue);
return namedCatchAllParamValue;
}
@@ -3230,10 +3231,12 @@
private Template template;
Namespace() {
+ super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
this.template = Environment.this.getTemplate();
}
Namespace(Template template) {
+ super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
this.template = template;
}
diff --git a/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java b/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
index d8cd611..398f382 100644
--- a/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
+++ b/src/main/java/freemarker/core/ExtendedDecimalFormatParser.java
@@ -87,10 +87,6 @@
+ PARAM_VALUE_RND_HALF_EVEN + ", " + PARAM_VALUE_RND_UNNECESSARY);
}
- if (_JavaVersions.JAVA_6 == null) {
- throw new InvalidParameterValueException("For setting the rounding mode you need Java 6 or later.");
- }
-
parser.roundingMode = parsedValue;
}
});
@@ -136,11 +132,7 @@
m.put(PARAM_EXPONENT_SEPARATOR, new ParameterHandler() {
public void handle(ExtendedDecimalFormatParser parser, String value)
throws InvalidParameterValueException {
- if (_JavaVersions.JAVA_6 == null) {
- throw new InvalidParameterValueException(
- "For setting the exponent separator you need Java 6 or later.");
- }
- _JavaVersions.JAVA_6.setExponentSeparator(parser.symbols, value);
+ parser.symbols.setExponentSeparator(value);
}
});
m.put(PARAM_MINUS_SIGN, new ParameterHandler() {
@@ -241,10 +233,7 @@
}
if (roundingMode != null) {
- if (_JavaVersions.JAVA_6 == null) {
- throw new ParseException("Setting rounding mode needs Java 6 or later", 0);
- }
- _JavaVersions.JAVA_6.setRoundingMode(decimalFormat, roundingMode);
+ decimalFormat.setRoundingMode(roundingMode);
}
if (multiplier != null) {
diff --git a/src/main/java/freemarker/core/GetOptionalTemplateMethod.java b/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
index 158b08c..be6c822 100644
--- a/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
+++ b/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
@@ -37,6 +37,7 @@
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.TemplateModelUtils;
/**
@@ -142,7 +143,7 @@
"; see cause exception");
}
- SimpleHash result = new SimpleHash(env.getObjectWrapper());
+ SimpleHash result = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
result.put(RESULT_EXISTS, template != null);
// If the template is missing, result.include and such will be missing too, so that a default can be
// conveniently provided like in <@optTemp.include!myDefaultMacro />.
diff --git a/src/main/java/freemarker/core/HashLiteral.java b/src/main/java/freemarker/core/HashLiteral.java
index 113b94d..7b4607c 100644
--- a/src/main/java/freemarker/core/HashLiteral.java
+++ b/src/main/java/freemarker/core/HashLiteral.java
@@ -127,8 +127,8 @@
// Legacy hash literal, where repeated keys were kept when doing ?values or ?keys, yet overwritten when
// doing hash[key].
map = new HashMap<String, TemplateModel>();
- List<String> keyList = new ArrayList<String>(size);
- List<TemplateModel> valueList = new ArrayList<TemplateModel>(size);
+ SimpleSequence keyList = new SimpleSequence(size, _TemplateAPI.SAFE_OBJECT_WRAPPER);
+ SimpleSequence valueList = new SimpleSequence(size, _TemplateAPI.SAFE_OBJECT_WRAPPER);
for (int i = 0; i < size; i++) {
Expression keyExp = keys.get(i);
Expression valExp = values.get(i);
@@ -141,8 +141,8 @@
keyList.add(key);
valueList.add(value);
}
- keyCollection = new CollectionAndSequence(new SimpleSequence(keyList));
- valueCollection = new CollectionAndSequence(new SimpleSequence(valueList));
+ keyCollection = new CollectionAndSequence(keyList);
+ valueCollection = new CollectionAndSequence(valueList);
}
}
@@ -153,7 +153,8 @@
public TemplateCollectionModel keys() {
if (keyCollection == null) {
// This can only happen when IcI >= 2.3.21, an the map is a LinkedHashMap.
- keyCollection = new CollectionAndSequence(new SimpleSequence(map.keySet()));
+ keyCollection = new CollectionAndSequence(
+ new SimpleSequence(map.keySet(), _TemplateAPI.SAFE_OBJECT_WRAPPER));
}
return keyCollection;
}
@@ -161,7 +162,8 @@
public TemplateCollectionModel values() {
if (valueCollection == null) {
// This can only happen when IcI >= 2.3.21, an the map is a LinkedHashMap.
- valueCollection = new CollectionAndSequence(new SimpleSequence(map.values()));
+ valueCollection = new CollectionAndSequence(
+ new SimpleSequence(map.values(), _TemplateAPI.SAFE_OBJECT_WRAPPER));
}
return valueCollection;
}
diff --git a/src/main/java/freemarker/core/ListLiteral.java b/src/main/java/freemarker/core/ListLiteral.java
index 02b42be..e384cd0 100644
--- a/src/main/java/freemarker/core/ListLiteral.java
+++ b/src/main/java/freemarker/core/ListLiteral.java
@@ -22,7 +22,6 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
@@ -32,6 +31,7 @@
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
final class ListLiteral extends Expression {
@@ -44,11 +44,10 @@
@Override
TemplateModel _eval(Environment env) throws TemplateException {
- SimpleSequence list = new SimpleSequence(items.size());
- for (Iterator it = items.iterator(); it.hasNext(); ) {
- Expression exp = (Expression) it.next();
+ SimpleSequence list = new SimpleSequence(items.size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
+ for (Expression exp : items) {
TemplateModel tm = exp.eval(env);
- if (env == null || !env.isClassicCompatible()) {
+ if (env == null || !env.isClassicCompatible()) {
exp.assertNonNull(tm, env);
}
list.add(tm);
@@ -141,7 +140,7 @@
TemplateSequenceModel evaluateStringsToNamespaces(Environment env) throws TemplateException {
TemplateSequenceModel val = (TemplateSequenceModel) eval(env);
- SimpleSequence result = new SimpleSequence(val.size());
+ SimpleSequence result = new SimpleSequence(val.size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
for (int i = 0; i < items.size(); i++) {
Object itemExpr = items.get(i);
if (itemExpr instanceof StringLiteral) {
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index 4e564ef..79b0c83 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -37,6 +37,7 @@
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.Constants;
/**
@@ -342,7 +343,8 @@
lengthWithCatchAlls += ((TemplateSequenceModel) catchAllArgValue).size();
}
- SimpleSequence argsSpecVarValue = new SimpleSequence(lengthWithCatchAlls);
+ SimpleSequence argsSpecVarValue = new SimpleSequence(
+ lengthWithCatchAlls, _TemplateAPI.SAFE_OBJECT_WRAPPER);
for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) {
argsSpecVarValue.add(argsSpecVarDraft[paramIndex]);
}
@@ -377,7 +379,7 @@
SimpleHash argsSpecVarValue = new SimpleHash(
new LinkedHashMap<String, Object>(lengthWithCatchAlls * 4 / 3, 1.0f),
- null, 0);
+ _TemplateAPI.SAFE_OBJECT_WRAPPER, 0);
for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) {
argsSpecVarValue.put(paramNames[paramIndex], argsSpecVarDraft[paramIndex]);
}
diff --git a/src/main/java/freemarker/core/RecurseNode.java b/src/main/java/freemarker/core/RecurseNode.java
index 42a8d32..c7a4b09 100644
--- a/src/main/java/freemarker/core/RecurseNode.java
+++ b/src/main/java/freemarker/core/RecurseNode.java
@@ -28,6 +28,7 @@
import freemarker.template.TemplateNodeModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
/**
@@ -57,7 +58,7 @@
}
if (nss != null) {
if (nss instanceof TemplateHashModel) {
- SimpleSequence ss = new SimpleSequence(1);
+ SimpleSequence ss = new SimpleSequence(1, _TemplateAPI.SAFE_OBJECT_WRAPPER);
ss.add(nss);
nss = ss;
} else if (!(nss instanceof TemplateSequenceModel)) {
diff --git a/src/main/java/freemarker/core/TemplateElement.java b/src/main/java/freemarker/core/TemplateElement.java
index 5f90b60..6cb9b54 100644
--- a/src/main/java/freemarker/core/TemplateElement.java
+++ b/src/main/java/freemarker/core/TemplateElement.java
@@ -22,7 +22,6 @@
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
-import java.util.Map;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateException;
@@ -53,7 +52,7 @@
* Contains 1 or more nested elements with optional trailing {@code null}-s, or is {@code null} exactly if there are
* no nested elements. Normally, the {@link #parent} of these is the {@code this}, however, in some exceptional
* cases it's not so, to avoid copying the whole descendant tree with a different parent (as in the result of
- * {@link Macro#Macro(Macro, Map)}.
+ * {@link Macro#Macro(Macro, Macro.WithArgs)}.
*/
private TemplateElement[] childBuffer;
diff --git a/src/main/java/freemarker/core/VisitNode.java b/src/main/java/freemarker/core/VisitNode.java
index be80fe2..daaf0a7 100644
--- a/src/main/java/freemarker/core/VisitNode.java
+++ b/src/main/java/freemarker/core/VisitNode.java
@@ -27,6 +27,7 @@
import freemarker.template.TemplateNodeModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
/**
@@ -56,7 +57,7 @@
}
if (nss != null) {
if (nss instanceof Environment.Namespace) {
- SimpleSequence ss = new SimpleSequence(1);
+ SimpleSequence ss = new SimpleSequence(1, _TemplateAPI.SAFE_OBJECT_WRAPPER);
ss.add(nss);
nss = ss;
} else if (!(nss instanceof TemplateSequenceModel)) {
diff --git a/src/main/java/freemarker/core/_Java6Impl.java b/src/main/java/freemarker/core/_Java6Impl.java
deleted file mode 100644
index 090a58d..0000000
--- a/src/main/java/freemarker/core/_Java6Impl.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package freemarker.core;
-
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-
-/**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
- */
-//Compile this against Java 6
-@SuppressWarnings("Since15") // For IntelliJ inspection
-public final class _Java6Impl implements _Java6 {
-
- public static final _Java6 INSTANCE = new _Java6Impl();
-
- private _Java6Impl() {
- // Not meant to be instantiated
- }
-
- public void setRoundingMode(DecimalFormat df, RoundingMode roundingMode) {
- df.setRoundingMode(roundingMode);
- }
-
- public void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator) {
- dfs.setExponentSeparator(exponentSeparator);
- }
-
-}
diff --git a/src/main/java/freemarker/core/_JavaVersions.java b/src/main/java/freemarker/core/_JavaVersions.java
index 7e6f1eb..3f07673 100644
--- a/src/main/java/freemarker/core/_JavaVersions.java
+++ b/src/main/java/freemarker/core/_JavaVersions.java
@@ -31,49 +31,6 @@
// Not meant to be instantiated
}
- private static final boolean IS_AT_LEAST_6;
- static {
- boolean result = false;
- String vStr = SecurityUtilities.getSystemProperty("java.version", null);
- if (vStr != null) {
- try {
- Version v = new Version(vStr);
- result = v.getMajor() == 1 && v.getMinor() >= 6 || v.getMajor() > 1;
- } catch (Exception e) {
- // Ignore
- }
- }
- if (vStr == null) {
- try {
- Class.forName("java.util.ServiceLoader");
- result = true;
- } catch (Exception e) {
- // Ignore
- }
- }
- IS_AT_LEAST_6 = result;
- }
-
- static public final _Java6 JAVA_6;
- static {
- _Java6 java6;
- if (IS_AT_LEAST_6) {
- try {
- java6 = (_Java6) Class.forName("freemarker.core._Java6Impl").getField("INSTANCE").get(null);
- } catch (Exception e) {
- try {
- Logger.getLogger("freemarker.runtime").error("Failed to access Java 6 functionality", e);
- } catch (Exception e2) {
- // Suppressed
- }
- java6 = null;
- }
- } else {
- java6 = null;
- }
- JAVA_6 = java6;
- }
-
private static final boolean IS_AT_LEAST_8;
static {
boolean result = false;
diff --git a/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java b/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java
index 59fed7b..c8943e5 100644
--- a/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java
+++ b/src/main/java/freemarker/ext/ant/FreemarkerXmlTask.java
@@ -49,6 +49,7 @@
import freemarker.template.Template;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateNodeModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.ClassUtil;
import freemarker.template.utility.SecurityUtilities;
@@ -603,7 +604,7 @@
}
private static TemplateModel wrapMap(Map table) {
- SimpleHash model = new SimpleHash();
+ SimpleHash model = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
for (Iterator it = table.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry) it.next();
model.put(String.valueOf(entry.getKey()), new SimpleScalar(String.valueOf(entry.getValue())));
diff --git a/src/main/java/freemarker/ext/beans/BeanModel.java b/src/main/java/freemarker/ext/beans/BeanModel.java
index 1f885fa..a6c5e4a 100644
--- a/src/main/java/freemarker/ext/beans/BeanModel.java
+++ b/src/main/java/freemarker/ext/beans/BeanModel.java
@@ -116,7 +116,7 @@
* matching the key name. If a method or property is found, it's wrapped
* into {@link freemarker.template.TemplateMethodModelEx} (for a method or
* indexed property), or evaluated on-the-fly and the return value wrapped
- * into appropriate model (for a non-indexed property) Models for various
+ * into appropriate model (for a non-indexed property). Models for various
* properties and methods are cached on a per-class basis, so the costly
* introspection is performed only once per property or method of a class.
* (Side-note: this also implies that any class whose method has been called
@@ -232,7 +232,7 @@
// cachedModel remains null, as we don't cache these
}
} else if (desc instanceof Field) {
- resultModel = wrapper.wrap(((Field) desc).get(object));
+ resultModel = wrapper.readField(object, (Field) desc);
// cachedModel remains null, as we don't cache these
} else if (desc instanceof Method) {
Method method = (Method) desc;
diff --git a/src/main/java/freemarker/ext/beans/BeansModelCache.java b/src/main/java/freemarker/ext/beans/BeansModelCache.java
index 7cf18e9..8d6cae5 100644
--- a/src/main/java/freemarker/ext/beans/BeansModelCache.java
+++ b/src/main/java/freemarker/ext/beans/BeansModelCache.java
@@ -30,8 +30,8 @@
import freemarker.template.TemplateModel;
public class BeansModelCache extends ModelCache {
- private final Map classToFactory = new ConcurrentHashMap();
- private final Set mappedClassNames = new HashSet();
+ private final Map<Class<?>, ModelFactory> classToFactory = new ConcurrentHashMap<Class<?>, ModelFactory>();
+ private final Set<String> mappedClassNames = new HashSet<String>();
private final BeansWrapper wrapper;
@@ -49,7 +49,7 @@
protected TemplateModel create(Object object) {
Class clazz = object.getClass();
- ModelFactory factory = (ModelFactory) classToFactory.get(clazz);
+ ModelFactory factory = classToFactory.get(clazz);
if (factory == null) {
// Synchronized so that we won't unnecessarily create the same factory for multiple times in parallel.
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java
index 586ee12..8190715 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapper.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java
@@ -24,6 +24,7 @@
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
@@ -96,7 +97,8 @@
/**
* At this level of exposure, all methods and properties of the
- * wrapped objects are exposed to the template.
+ * wrapped objects are exposed to the template, and the {@link MemberAccessPolicy}
+ * will be ignored.
*/
public static final int EXPOSE_ALL = 0;
@@ -351,7 +353,7 @@
// synchronize on, even during the classIntrospector is being replaced.
sharedIntrospectionLock = new Object();
classIntrospector = new ClassIntrospector(
- _BeansAPI.getClassIntrospectorBuilder(bwConf), sharedIntrospectionLock);
+ _BeansAPI.getClassIntrospectorBuilder(bwConf), sharedIntrospectionLock, false, false);
} else {
// As this is a read-only BeansWrapper, the classIntrospector is never replaced, and since it's shared by
// other BeansWrapper instances, we use the lock belonging to the shared ClassIntrospector.
@@ -657,6 +659,29 @@
}
}
+ /**
+ * @since 2.3.30
+ */
+ public MemberAccessPolicy getMemberAccessPolicy() {
+ return classIntrospector.getMemberAccessPolicy();
+ }
+
+ /**
+ * Sets the {@link MemberAccessPolicy}; default is {@link DefaultMemberAccessPolicy#getInstance(Version)}, which
+ * is not appropriate if template editors aren't trusted.
+ *
+ * @since 2.3.30
+ */
+ public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+ checkModifiable();
+
+ if (classIntrospector.getMemberAccessPolicy() != memberAccessPolicy) {
+ ClassIntrospectorBuilder builder = classIntrospector.createBuilder();
+ builder.setMemberAccessPolicy(memberAccessPolicy);
+ replaceClassIntrospector(builder);
+ }
+ }
+
MethodSorter getMethodSorter() {
return classIntrospector.getMethodSorter();
}
@@ -682,7 +707,7 @@
* @since 2.3.21
*/
public boolean isClassIntrospectionCacheRestricted() {
- return classIntrospector.getHasSharedInstanceRestrictons();
+ return classIntrospector.getHasSharedInstanceRestrictions();
}
/**
@@ -692,7 +717,7 @@
private void replaceClassIntrospector(ClassIntrospectorBuilder builder) {
checkModifiable();
- final ClassIntrospector newCI = new ClassIntrospector(builder, sharedIntrospectionLock);
+ final ClassIntrospector newCI = new ClassIntrospector(builder, sharedIntrospectionLock, false, false);
final ClassIntrospector oldCI;
// In principle this need not be synchronized, but as apps might publish the configuration improperly, or
@@ -858,9 +883,6 @@
*/
protected static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
_TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
- if (incompatibleImprovements.intValue() < _TemplateAPI.VERSION_INT_2_3_0) {
- throw new IllegalArgumentException("Version must be at least 2.3.0.");
- }
return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_27 ? Configuration.VERSION_2_3_27
: incompatibleImprovements.intValue() == _TemplateAPI.VERSION_INT_2_3_26 ? Configuration.VERSION_2_3_26
: is2324Bugfixed(incompatibleImprovements) ? Configuration.VERSION_2_3_24
@@ -1479,12 +1501,23 @@
}
}
}
-
+
/**
- * Invokes the specified method, wrapping the return value. The specialty
- * of this method is that if the return value is null, and the return type
- * of the invoked method is void, {@link TemplateModel#NOTHING} is returned.
- * @param object the object to invoke the method on
+ * Invokes the specified method, wrapping the return value. All method invocations done in templates should go
+ * through this (assuming the target object was wrapped with this {@link ObjectWrapper}).
+ *
+ * <p>This method is protected since 2.3.30; before that it was package private. The intended application of
+ * overriding this is monitoring what calls are made from templates. That can be useful to asses what will be needed
+ * in a {@link WhitelistMemberAccessPolicy} for example. Note that {@link Object#toString} calls caused by type
+ * conversion (like when you have <code>${myObject}</code>) will not go through here, as they aren't called by the
+ * template directly (and aren't called via reflection). On the other hand, <code>${myObject[key]}</code>,
+ * if {@code myObject} is not a {@link Map}, will go through here as a {@code get(String|Object)} method call, if
+ * there's a such method.
+ *
+ * <p>If the return value is null, and the return type of the invoked method is void,
+ * {@link TemplateModel#NOTHING} is returned.
+ *
+ * @param object the object to invoke the method on ({@code null} may be null for static methods)
* @param method the method to invoke
* @param args the arguments to the method
* @return the wrapped return value of the method.
@@ -1495,9 +1528,13 @@
* (this can happen if the wrapper has an outer identity or is subclassed,
* and the outer identity or the subclass throws an exception. Plain
* BeansWrapper never throws TemplateModelException).
+ *
+ * @see #readField(Object, Field)
+ *
+ * @since 2.3.30
*/
- TemplateModel invokeMethod(Object object, Method method, Object[] args)
- throws InvocationTargetException,
+ protected TemplateModel invokeMethod(Object object, Method method, Object[] args)
+ throws InvocationTargetException,
IllegalAccessException,
TemplateModelException {
// [2.4]: Java's Method.invoke truncates numbers if the target type has not enough bits to hold the value.
@@ -1509,6 +1546,24 @@
: getOuterIdentity().wrap(retval);
}
+ /**
+ * Reads the specified field, returns its value as {@link TemplateModel}. All field reading done in templates
+ * should go through this (assuming the target object was wrapped with this {@link ObjectWrapper}).
+ *
+ * <p>Just like in the case of {@link #invokeMethod(Object, Method, Object[])}, overriding this can be useful if you
+ * want to monitor what members are accessed by templates. However, it has the caveat that final field values are
+ * possibly cached, so you won't see all reads. Furthermore, at least static models pre-read final fields, so
+ * they will be read even if the templates don't read them.
+ *
+ * @see #invokeMethod(Object, Method, Object[])
+ *
+ * @since 2.3.30
+ */
+ protected TemplateModel readField(Object object, Field field)
+ throws IllegalAccessException, TemplateModelException {
+ return getOuterIdentity().wrap(field.get(object));
+ }
+
/**
* Returns a hash model that represents the so-called class static models.
* Every class static model is itself a hash through which you can call
@@ -1525,8 +1580,7 @@
public TemplateHashModel getStaticModels() {
return staticModels;
}
-
-
+
/**
* Returns a hash model that represents the so-called class enum models.
* Every class' enum model is itself a hash through which you can access
@@ -1569,7 +1623,7 @@
Object ctors = classIntrospector.get(clazz).get(ClassIntrospector.CONSTRUCTORS_KEY);
if (ctors == null) {
throw new TemplateModelException("Class " + clazz.getName() +
- " has no public constructors.");
+ " has no exposed constructors.");
}
Constructor<?> ctor = null;
Object[] objargs;
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java b/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java
index 047a9dd..6072a1e 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapperBuilder.java
@@ -75,9 +75,12 @@
* {@code freemarker.jar}-s (typically, in two Web Application's {@code WEB-INF/lib} directories), those won't
* share their caches (as they don't share the same FreeMarker classes).
* Also, currently there's a separate cache for each permutation of the property values that influence class
- * introspection: {@link BeansWrapperBuilder#setExposeFields(boolean) expose_fields} and
- * {@link BeansWrapperBuilder#setExposureLevel(int) exposure_level}. So only {@link BeansWrapper} where those
- * properties are the same may share class introspection caches among each other.
+ * introspection:
+ * {@link BeansWrapperBuilder#setExposeFields(boolean) expose_fields}, and
+ * {@link BeansWrapperBuilder#setExposureLevel(int) exposure_level}, and
+ * {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} member_access_policy}.
+ * So only {@link BeansWrapper} where those properties are the same may share class introspection caches among each
+ * other.
* </li>
* <li><p>Model caches: These are local to a {@link BeansWrapper}. {@link BeansWrapperBuilder} returns the same
* {@link BeansWrapper} instance for equivalent properties (unless the existing instance was garbage collected
diff --git a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
index 905bde9..323275e 100644
--- a/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
+++ b/src/main/java/freemarker/ext/beans/BeansWrapperConfiguration.java
@@ -225,10 +225,19 @@
classIntrospectorBuilder.setExposeFields(exposeFields);
}
+ public MemberAccessPolicy getMemberAccessPolicy() {
+ return classIntrospectorBuilder.getMemberAccessPolicy();
+ }
+
+ /** See {@link BeansWrapper#setMemberAccessPolicy(MemberAccessPolicy)}. */
+ public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+ classIntrospectorBuilder.setMemberAccessPolicy(memberAccessPolicy);
+ }
+
public boolean getTreatDefaultMethodsAsBeanMembers() {
return classIntrospectorBuilder.getTreatDefaultMethodsAsBeanMembers();
}
-
+
/** See {@link BeansWrapper#setTreatDefaultMethodsAsBeanMembers(boolean)} */
public void setTreatDefaultMethodsAsBeanMembers(boolean treatDefaultMethodsAsBeanMembers) {
classIntrospectorBuilder.setTreatDefaultMethodsAsBeanMembers(treatDefaultMethodsAsBeanMembers);
diff --git a/src/main/java/freemarker/ext/beans/BlacklistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/BlacklistMemberAccessPolicy.java
new file mode 100644
index 0000000..69d0410
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/BlacklistMemberAccessPolicy.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.util.Collection;
+
+/**
+ * Blacklist-based member access policy, that is, members that are matched by the listing will not be accessible, all
+ * others will be. Note that {@link BeansWrapper} and its subclasses doesn't discover all members on the first place,
+ * and the {@link MemberAccessPolicy} just removes from that set of members, never adds to it.
+ *
+ * <p>This class is rarely useful in itself, and mostly meant to be used when composing a {@link MemberAccessPolicy}
+ * from other {@link MemberAccessPolicy}-es. If you are serious about security, never use this alone; consider using
+ * {@link WhitelistMemberAccessPolicy} as part of your solution.
+ *
+ * <p>See more about the rules at {@link MemberSelectorListMemberAccessPolicy}. Unlike
+ * {@link WhitelistMemberAccessPolicy}, {@link BlacklistMemberAccessPolicy} doesn't have annotations that can be used
+ * to add members to the member selector list.
+ *
+ * @since 2.3.30
+ */
+public class BlacklistMemberAccessPolicy extends MemberSelectorListMemberAccessPolicy {
+
+ /**
+ * @param memberSelectors
+ * List of member selectors; see {@link MemberSelectorListMemberAccessPolicy} class-level documentation for
+ * more.
+ */
+ public BlacklistMemberAccessPolicy(Collection<? extends MemberSelector> memberSelectors) {
+ super(memberSelectors, ListType.BLACKLIST, null);
+ }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java b/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java
index cc4a258..fd836d4 100644
--- a/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java
+++ b/src/main/java/freemarker/ext/beans/ClassBasedModelFactory.java
@@ -37,8 +37,8 @@
abstract class ClassBasedModelFactory implements TemplateHashModel {
private final BeansWrapper wrapper;
- private final Map/*<String,TemplateModel>*/ cache = new ConcurrentHashMap();
- private final Set classIntrospectionsInProgress = new HashSet();
+ private final Map<String,TemplateModel> cache = new ConcurrentHashMap<String,TemplateModel>();
+ private final Set<String> classIntrospectionsInProgress = new HashSet<String>();
protected ClassBasedModelFactory(BeansWrapper wrapper) {
this.wrapper = wrapper;
@@ -59,7 +59,7 @@
private TemplateModel getInternal(String key) throws TemplateModelException, ClassNotFoundException {
{
- TemplateModel model = (TemplateModel) cache.get(key);
+ TemplateModel model = cache.get(key);
if (model != null) return model;
}
@@ -67,19 +67,17 @@
int classIntrospectorClearingCounter;
final Object sharedLock = wrapper.getSharedIntrospectionLock();
synchronized (sharedLock) {
- TemplateModel model = (TemplateModel) cache.get(key);
+ TemplateModel model = cache.get(key);
if (model != null) return model;
- while (model == null
- && classIntrospectionsInProgress.contains(key)) {
+ while (model == null && classIntrospectionsInProgress.contains(key)) {
// Another thread is already introspecting this class;
// waiting for its result.
try {
sharedLock.wait();
- model = (TemplateModel) cache.get(key);
+ model = cache.get(key);
} catch (InterruptedException e) {
- throw new RuntimeException(
- "Class inrospection data lookup aborded: " + e);
+ throw new RuntimeException("Class inrospection data lookup aborted: " + e);
}
}
if (model != null) return model;
@@ -93,7 +91,7 @@
classIntrospectorClearingCounter = classIntrospector.getClearingCounter();
}
try {
- final Class clazz = ClassUtil.forName(key);
+ final Class<?> clazz = ClassUtil.forName(key);
// This is called so that we trigger the
// class-reloading detector. If clazz is a reloaded class,
@@ -129,7 +127,7 @@
}
}
- void removeFromCache(Class clazz) {
+ void removeFromCache(Class<?> clazz) {
synchronized (wrapper.getSharedIntrospectionLock()) {
cache.remove(clazz.getName());
}
@@ -139,8 +137,7 @@
return false;
}
- protected abstract TemplateModel createModel(Class clazz)
- throws TemplateModelException;
+ protected abstract TemplateModel createModel(Class<?> clazz) throws TemplateModelException;
protected BeansWrapper getWrapper() {
return wrapper;
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
index a31d430..630bf95 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java
@@ -53,6 +53,7 @@
import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput;
import freemarker.ext.util.ModelCache;
import freemarker.log.Logger;
+import freemarker.template.Version;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.SecurityUtilities;
@@ -77,6 +78,11 @@
private static final String JREBEL_INTEGRATION_ERROR_MSG
= "Error initializing JRebel integration. JRebel integration disabled.";
+ private static final ExecutableMemberSignature GET_STRING_SIGNATURE =
+ new ExecutableMemberSignature("get", new Class[] { String.class });
+ private static final ExecutableMemberSignature GET_OBJECT_SIGNATURE =
+ new ExecutableMemberSignature("get", new Class[] { Object.class });
+
/**
* When this property is true, some things are stricter. This is mostly to catch suspicious things in development
* that can otherwise be valid situations.
@@ -138,13 +144,14 @@
final int exposureLevel;
final boolean exposeFields;
+ final MemberAccessPolicy memberAccessPolicy;
final MethodAppearanceFineTuner methodAppearanceFineTuner;
final MethodSorter methodSorter;
final boolean treatDefaultMethodsAsBeanMembers;
- final boolean bugfixed;
+ final Version incompatibleImprovements;
- /** See {@link #getHasSharedInstanceRestrictons()} */
- final private boolean hasSharedInstanceRestrictons;
+ /** See {@link #getHasSharedInstanceRestrictions()} */
+ final private boolean hasSharedInstanceRestrictions;
/** See {@link #isShared()} */
final private boolean shared;
@@ -168,34 +175,25 @@
// Instantiation:
/**
- * Creates a new instance, that is hence surely not shared (singleton) instance.
- *
- * @param pa
- * Stores what the values of the JavaBean properties of the returned instance will be. Not {@code null}.
- */
- ClassIntrospector(ClassIntrospectorBuilder pa, Object sharedLock) {
- this(pa, sharedLock, false, false);
- }
-
- /**
- * @param hasSharedInstanceRestrictons
+ * @param hasSharedInstanceRestrictions
* {@code true} exactly if we are creating a new instance with {@link ClassIntrospectorBuilder}. Then
* it's {@code true} even if it won't put the instance into the cache.
*/
ClassIntrospector(ClassIntrospectorBuilder builder, Object sharedLock,
- boolean hasSharedInstanceRestrictons, boolean shared) {
+ boolean hasSharedInstanceRestrictions, boolean shared) {
NullArgumentException.check("sharedLock", sharedLock);
this.exposureLevel = builder.getExposureLevel();
this.exposeFields = builder.getExposeFields();
+ this.memberAccessPolicy = builder.getMemberAccessPolicy();
this.methodAppearanceFineTuner = builder.getMethodAppearanceFineTuner();
this.methodSorter = builder.getMethodSorter();
this.treatDefaultMethodsAsBeanMembers = builder.getTreatDefaultMethodsAsBeanMembers();
- this.bugfixed = builder.isBugfixed();
+ this.incompatibleImprovements = builder.getIncompatibleImprovements();
this.sharedLock = sharedLock;
- this.hasSharedInstanceRestrictons = hasSharedInstanceRestrictons;
+ this.hasSharedInstanceRestrictions = hasSharedInstanceRestrictions;
this.shared = shared;
if (CLASS_CHANGE_NOTIFIER != null) {
@@ -204,14 +202,15 @@
}
/**
- * Returns a {@link ClassIntrospectorBuilder}-s that could be used to create an identical {@link #ClassIntrospector}
- * . The returned {@link ClassIntrospectorBuilder} can be modified without interfering with anything.
+ * Returns a {@link ClassIntrospectorBuilder} that could be used to create an identical
+ * {@link #ClassIntrospector}. The returned {@link ClassIntrospectorBuilder} can be modified without interfering
+ * with anything.
*/
ClassIntrospectorBuilder createBuilder() {
return new ClassIntrospectorBuilder(this);
}
- // ------------------------------------------------------------------------------------------------------------------
+ // -----------------------------------------------------------------------------------------------------------------
// Introspection:
/**
@@ -273,25 +272,26 @@
*/
private Map<Object, Object> createClassIntrospectionData(Class<?> clazz) {
final Map<Object, Object> introspData = new HashMap<Object, Object>();
+ ClassMemberAccessPolicy classMemberAccessPolicy = getClassMemberAccessPolicyIfNotIgnored(clazz);
if (exposeFields) {
- addFieldsToClassIntrospectionData(introspData, clazz);
+ addFieldsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy);
}
- final Map<MethodSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
+ final Map<ExecutableMemberSignature, List<Method>> accessibleMethods = discoverAccessibleMethods(clazz);
- addGenericGetToClassIntrospectionData(introspData, accessibleMethods);
+ addGenericGetToClassIntrospectionData(introspData, accessibleMethods, classMemberAccessPolicy);
if (exposureLevel != BeansWrapper.EXPOSE_NOTHING) {
try {
- addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods);
+ addBeanInfoToClassIntrospectionData(introspData, clazz, accessibleMethods, classMemberAccessPolicy);
} catch (IntrospectionException e) {
LOG.warn("Couldn't properly perform introspection for class " + clazz, e);
introspData.clear(); // FIXME NBC: Don't drop everything here.
}
}
- addConstructorsToClassIntrospectionData(introspData, clazz);
+ addConstructorsToClassIntrospectionData(introspData, clazz, classMemberAccessPolicy);
if (introspData.size() > 1) {
return introspData;
@@ -303,28 +303,31 @@
}
}
- private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz)
- throws SecurityException {
+ private void addFieldsToClassIntrospectionData(Map<Object, Object> introspData, Class<?> clazz,
+ ClassMemberAccessPolicy classMemberAccessPolicy) throws SecurityException {
Field[] fields = clazz.getFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if ((field.getModifiers() & Modifier.STATIC) == 0) {
- introspData.put(field.getName(), field);
+ if (classMemberAccessPolicy == null || classMemberAccessPolicy.isFieldExposed(field)) {
+ introspData.put(field.getName(), field);
+ }
}
}
}
private void addBeanInfoToClassIntrospectionData(
- Map<Object, Object> introspData, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods)
- throws IntrospectionException {
+ Map<Object, Object> introspData, Class<?> clazz,
+ Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+ ClassMemberAccessPolicy classMemberAccessPolicy) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
List<PropertyDescriptor> pdas = getPropertyDescriptors(beanInfo, clazz);
int pdasLength = pdas.size();
// Reverse order shouldn't mater, but we keep it to not risk backward incompatibility.
for (int i = pdasLength - 1; i >= 0; --i) {
addPropertyDescriptorToClassIntrospectionData(
- introspData, pdas.get(i), clazz,
- accessibleMethods);
+ introspData, pdas.get(i),
+ accessibleMethods, classMemberAccessPolicy);
}
if (exposureLevel < BeansWrapper.EXPOSE_PROPERTIES_ONLY) {
@@ -336,7 +339,7 @@
IdentityHashMap<Method, Void> argTypesUsedByIndexerPropReaders = null;
for (int i = mdsSize - 1; i >= 0; --i) {
final Method method = getMatchingAccessibleMethod(mds.get(i).getMethod(), accessibleMethods);
- if (method != null && isAllowedToExpose(method)) {
+ if (method != null && (isMethodExposed(classMemberAccessPolicy, method))) {
decision.setDefaults(method);
if (methodAppearanceFineTuner != null) {
if (decisionInput == null) {
@@ -353,7 +356,7 @@
(decision.getReplaceExistingProperty()
|| !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor))) {
addPropertyDescriptorToClassIntrospectionData(
- introspData, propDesc, clazz, accessibleMethods);
+ introspData, propDesc, accessibleMethods, classMemberAccessPolicy);
}
String methodKey = decision.getExposeMethodAs();
@@ -361,7 +364,8 @@
Object previous = introspData.get(methodKey);
if (previous instanceof Method) {
// Overloaded method - replace Method with a OverloadedMethods
- OverloadedMethods overloadedMethods = new OverloadedMethods(bugfixed);
+ OverloadedMethods overloadedMethods =
+ new OverloadedMethods(is2321Bugfixed());
overloadedMethods.addMethod((Method) previous);
overloadedMethods.addMethod(method);
introspData.put(methodKey, overloadedMethods);
@@ -661,9 +665,11 @@
}
private void addPropertyDescriptorToClassIntrospectionData(Map<Object, Object> introspData,
- PropertyDescriptor pd, Class<?> clazz, Map<MethodSignature, List<Method>> accessibleMethods) {
+ PropertyDescriptor pd,
+ Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+ ClassMemberAccessPolicy classMemberAccessPolicy) {
Method readMethod = getMatchingAccessibleMethod(pd.getReadMethod(), accessibleMethods);
- if (readMethod != null && !isAllowedToExpose(readMethod)) {
+ if (readMethod != null && !isMethodExposed(classMemberAccessPolicy, readMethod)) {
readMethod = null;
}
@@ -671,7 +677,7 @@
if (pd instanceof IndexedPropertyDescriptor) {
indexedReadMethod = getMatchingAccessibleMethod(
((IndexedPropertyDescriptor) pd).getIndexedReadMethod(), accessibleMethods);
- if (indexedReadMethod != null && !isAllowedToExpose(indexedReadMethod)) {
+ if (indexedReadMethod != null && !isMethodExposed(classMemberAccessPolicy, indexedReadMethod)) {
indexedReadMethod = null;
}
if (indexedReadMethod != null) {
@@ -688,31 +694,41 @@
}
private void addGenericGetToClassIntrospectionData(Map<Object, Object> introspData,
- Map<MethodSignature, List<Method>> accessibleMethods) {
- Method genericGet = getFirstAccessibleMethod(
- MethodSignature.GET_STRING_SIGNATURE, accessibleMethods);
+ Map<ExecutableMemberSignature, List<Method>> accessibleMethods,
+ ClassMemberAccessPolicy classMemberAccessPolicy) {
+ Method genericGet = getFirstAccessibleMethod(GET_STRING_SIGNATURE, accessibleMethods);
if (genericGet == null) {
- genericGet = getFirstAccessibleMethod(
- MethodSignature.GET_OBJECT_SIGNATURE, accessibleMethods);
+ genericGet = getFirstAccessibleMethod(GET_OBJECT_SIGNATURE, accessibleMethods);
}
- if (genericGet != null) {
+ if (genericGet != null && isMethodExposed(classMemberAccessPolicy, genericGet)) {
introspData.put(GENERIC_GET_KEY, genericGet);
}
}
private void addConstructorsToClassIntrospectionData(final Map<Object, Object> introspData,
- Class<?> clazz) {
+ Class<?> clazz, ClassMemberAccessPolicy classMemberAccessPolicy) {
try {
- Constructor<?>[] ctors = clazz.getConstructors();
- if (ctors.length == 1) {
- Constructor<?> ctor = ctors[0];
- introspData.put(CONSTRUCTORS_KEY, new SimpleMethod(ctor, ctor.getParameterTypes()));
- } else if (ctors.length > 1) {
- OverloadedMethods overloadedCtors = new OverloadedMethods(bugfixed);
- for (int i = 0; i < ctors.length; i++) {
- overloadedCtors.addConstructor(ctors[i]);
+ Constructor<?>[] ctorsUnfiltered = clazz.getConstructors();
+ List<Constructor<?>> ctors = new ArrayList<Constructor<?>>(ctorsUnfiltered.length);
+ for (Constructor<?> ctor : ctorsUnfiltered) {
+ if (classMemberAccessPolicy == null || classMemberAccessPolicy.isConstructorExposed(ctor)) {
+ ctors.add(ctor);
}
- introspData.put(CONSTRUCTORS_KEY, overloadedCtors);
+ }
+
+ if (!ctors.isEmpty()) {
+ final Object ctorsIntrospData;
+ if (ctors.size() == 1) {
+ Constructor<?> ctor = ctors.get(0);
+ ctorsIntrospData = new SimpleMethod(ctor, ctor.getParameterTypes());
+ } else {
+ OverloadedMethods overloadedCtors = new OverloadedMethods(is2321Bugfixed());
+ for (Constructor<?> ctor : ctors) {
+ overloadedCtors.addConstructor(ctor);
+ }
+ ctorsIntrospData = overloadedCtors;
+ }
+ introspData.put(CONSTRUCTORS_KEY, ctorsIntrospData);
}
} catch (SecurityException e) {
LOG.warn("Can't discover constructors for class " + clazz.getName(), e);
@@ -720,23 +736,24 @@
}
/**
- * Retrieves mapping of {@link MethodSignature}-s to a {@link List} of accessible methods for a class. In case the
- * class is not public, retrieves methods with same signature as its public methods from public superclasses and
- * interfaces. Basically upcasts every method to the nearest accessible method.
+ * Retrieves mapping of {@link ExecutableMemberSignature}-s to a {@link List} of accessible methods for a class. In
+ * case the class is not public, retrieves methods with same signature as its public methods from public
+ * superclasses and interfaces. Basically upcasts every method to the nearest accessible method.
*/
- private static Map<MethodSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
- Map<MethodSignature, List<Method>> accessibles = new HashMap<MethodSignature, List<Method>>();
+ private static Map<ExecutableMemberSignature, List<Method>> discoverAccessibleMethods(Class<?> clazz) {
+ Map<ExecutableMemberSignature, List<Method>> accessibles = new HashMap<ExecutableMemberSignature, List<Method>>();
discoverAccessibleMethods(clazz, accessibles);
return accessibles;
}
- private static void discoverAccessibleMethods(Class<?> clazz, Map<MethodSignature, List<Method>> accessibles) {
+ private static void discoverAccessibleMethods(
+ Class<?> clazz, Map<ExecutableMemberSignature, List<Method>> accessibles) {
if (Modifier.isPublic(clazz.getModifiers())) {
try {
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
- MethodSignature sig = new MethodSignature(method);
+ ExecutableMemberSignature sig = new ExecutableMemberSignature(method);
// Contrary to intuition, a class can actually have several
// different methods with same signature *but* different
// return types. These can't be constructed using Java the
@@ -775,11 +792,11 @@
}
}
- private static Method getMatchingAccessibleMethod(Method m, Map<MethodSignature, List<Method>> accessibles) {
+ private static Method getMatchingAccessibleMethod(Method m, Map<ExecutableMemberSignature, List<Method>> accessibles) {
if (m == null) {
return null;
}
- MethodSignature sig = new MethodSignature(m);
+ ExecutableMemberSignature sig = new ExecutableMemberSignature(m);
List<Method> ams = accessibles.get(sig);
if (ams == null) {
return null;
@@ -792,7 +809,8 @@
return null;
}
- private static Method getFirstAccessibleMethod(MethodSignature sig, Map<MethodSignature, List<Method>> accessibles) {
+ private static Method getFirstAccessibleMethod(
+ ExecutableMemberSignature sig, Map<ExecutableMemberSignature, List<Method>> accessibles) {
List<Method> ams = accessibles.get(sig);
if (ams == null || ams.isEmpty()) {
return null;
@@ -809,8 +827,28 @@
}
}
- boolean isAllowedToExpose(Method method) {
- return exposureLevel < BeansWrapper.EXPOSE_SAFE || !UnsafeMethods.isUnsafeMethod(method);
+ /**
+ * Returns the {@link ClassMemberAccessPolicy}, or {@code null} if it should be ignored because of other settings.
+ * (Ideally, all such rules should be contained in {@link ClassMemberAccessPolicy} alone, but that interface was
+ * added late in history.)
+ *
+ * @see #isMethodExposed(ClassMemberAccessPolicy, Method)
+ */
+ ClassMemberAccessPolicy getClassMemberAccessPolicyIfNotIgnored(Class containingClass) {
+ return exposureLevel < BeansWrapper.EXPOSE_SAFE ? null : memberAccessPolicy.forClass(containingClass);
+ }
+
+ /**
+ * @param classMemberAccessPolicyIfNotIgnored
+ * The value returned by {@link #getClassMemberAccessPolicyIfNotIgnored(Class)}
+ */
+ static boolean isMethodExposed(ClassMemberAccessPolicy classMemberAccessPolicyIfNotIgnored, Method method) {
+ return classMemberAccessPolicyIfNotIgnored == null
+ || classMemberAccessPolicyIfNotIgnored.isMethodExposed(method);
+ }
+
+ private boolean is2321Bugfixed() {
+ return BeansWrapper.is2321Bugfixed(incompatibleImprovements);
}
private static Map<Method, Class<?>[]> getArgTypesByMethod(Map<Object, Object> classInfo) {
@@ -823,39 +861,6 @@
return argTypes;
}
- private static final class MethodSignature {
- private static final MethodSignature GET_STRING_SIGNATURE =
- new MethodSignature("get", new Class[] { String.class });
- private static final MethodSignature GET_OBJECT_SIGNATURE =
- new MethodSignature("get", new Class[] { Object.class });
-
- private final String name;
- private final Class<?>[] args;
-
- private MethodSignature(String name, Class<?>[] args) {
- this.name = name;
- this.args = args;
- }
-
- MethodSignature(Method method) {
- this(method.getName(), method.getParameterTypes());
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof MethodSignature) {
- MethodSignature ms = (MethodSignature) o;
- return ms.name.equals(name) && Arrays.equals(args, ms.args);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return name.hashCode() ^ args.length; // TODO That's a poor quality hash... isn't this a problem?
- }
- }
-
// -----------------------------------------------------------------------------------------------------------------
// Cache management:
@@ -865,7 +870,7 @@
* @since 2.3.20
*/
void clearCache() {
- if (getHasSharedInstanceRestrictons()) {
+ if (getHasSharedInstanceRestrictions()) {
throw new IllegalStateException(
"It's not allowed to clear the whole cache in a read-only " + this.getClass().getName() +
"instance. Use removeFromClassIntrospectionCache(String prefix) instead.");
@@ -1044,7 +1049,11 @@
boolean getExposeFields() {
return exposeFields;
}
-
+
+ MemberAccessPolicy getMemberAccessPolicy() {
+ return memberAccessPolicy;
+ }
+
boolean getTreatDefaultMethodsAsBeanMembers() {
return treatDefaultMethodsAsBeanMembers;
}
@@ -1061,14 +1070,14 @@
* Returns {@code true} if this instance was created with {@link ClassIntrospectorBuilder}, even if it wasn't
* actually put into the cache (as we reserve the right to do so in later versions).
*/
- boolean getHasSharedInstanceRestrictons() {
- return hasSharedInstanceRestrictons;
+ boolean getHasSharedInstanceRestrictions() {
+ return hasSharedInstanceRestrictions;
}
/**
* Tells if this instance is (potentially) shared among {@link BeansWrapper} instances.
*
- * @see #getHasSharedInstanceRestrictons()
+ * @see #getHasSharedInstanceRestrictions()
*/
boolean isShared() {
return shared;
diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
index 25688e5..e2847ab 100644
--- a/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
+++ b/src/main/java/freemarker/ext/beans/ClassIntrospectorBuilder.java
@@ -26,19 +26,24 @@
import java.util.Iterator;
import java.util.Map;
+import freemarker.template.Configuration;
import freemarker.template.Version;
import freemarker.template._TemplateAPI;
+import freemarker.template.utility.NullArgumentException;
final class ClassIntrospectorBuilder implements Cloneable {
-
- private final boolean bugfixed;
- private static final Map/*<PropertyAssignments, Reference<ClassIntrospector>>*/ INSTANCE_CACHE = new HashMap();
- private static final ReferenceQueue INSTANCE_CACHE_REF_QUEUE = new ReferenceQueue();
-
+ private static final Map<ClassIntrospectorBuilder, Reference<ClassIntrospector>> INSTANCE_CACHE
+ = new HashMap<ClassIntrospectorBuilder, Reference<ClassIntrospector>>();
+ private static final ReferenceQueue<ClassIntrospector> INSTANCE_CACHE_REF_QUEUE
+ = new ReferenceQueue<ClassIntrospector>();
+
+ private final Version incompatibleImprovements;
+
// Properties and their *defaults*:
private int exposureLevel = BeansWrapper.EXPOSE_SAFE;
private boolean exposeFields;
+ private MemberAccessPolicy memberAccessPolicy;
private boolean treatDefaultMethodsAsBeanMembers;
private MethodAppearanceFineTuner methodAppearanceFineTuner;
private MethodSorter methodSorter;
@@ -49,23 +54,33 @@
// - If you add a new field, review all methods in this class, also the ClassIntrospector constructor
ClassIntrospectorBuilder(ClassIntrospector ci) {
- bugfixed = ci.bugfixed;
+ incompatibleImprovements = ci.incompatibleImprovements;
exposureLevel = ci.exposureLevel;
exposeFields = ci.exposeFields;
+ memberAccessPolicy = ci.memberAccessPolicy;
treatDefaultMethodsAsBeanMembers = ci.treatDefaultMethodsAsBeanMembers;
methodAppearanceFineTuner = ci.methodAppearanceFineTuner;
- methodSorter = ci.methodSorter;
+ methodSorter = ci.methodSorter;
}
ClassIntrospectorBuilder(Version incompatibleImprovements) {
// Warning: incompatibleImprovements must not affect this object at versions increments where there's no
// change in the BeansWrapper.normalizeIncompatibleImprovements results. That is, this class may don't react
- // to some version changes that affects BeansWrapper, but not the other way around.
- bugfixed = BeansWrapper.is2321Bugfixed(incompatibleImprovements);
+ // to some version changes that affects BeansWrapper, but not the other way around.
+ this.incompatibleImprovements = normalizeIncompatibleImprovementsVersion(incompatibleImprovements);
treatDefaultMethodsAsBeanMembers
= incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_26;
+ memberAccessPolicy = DefaultMemberAccessPolicy.getInstance(this.incompatibleImprovements);
}
-
+
+ private static Version normalizeIncompatibleImprovementsVersion(Version incompatibleImprovements) {
+ _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+ // All breakpoints here must occur in BeansWrapper.normalizeIncompatibleImprovements!
+ return incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_30 ? Configuration.VERSION_2_3_30
+ : incompatibleImprovements.intValue() >= _TemplateAPI.VERSION_INT_2_3_21 ? Configuration.VERSION_2_3_21
+ : Configuration.VERSION_2_3_0;
+ }
+
@Override
protected Object clone() {
try {
@@ -79,10 +94,11 @@
public int hashCode() {
final int prime = 31;
int result = 1;
- result = prime * result + (bugfixed ? 1231 : 1237);
+ result = prime * result + incompatibleImprovements.hashCode();
result = prime * result + (exposeFields ? 1231 : 1237);
result = prime * result + (treatDefaultMethodsAsBeanMembers ? 1231 : 1237);
result = prime * result + exposureLevel;
+ result = prime * result + memberAccessPolicy.hashCode();
result = prime * result + System.identityHashCode(methodAppearanceFineTuner);
result = prime * result + System.identityHashCode(methodSorter);
return result;
@@ -95,10 +111,11 @@
if (getClass() != obj.getClass()) return false;
ClassIntrospectorBuilder other = (ClassIntrospectorBuilder) obj;
- if (bugfixed != other.bugfixed) return false;
+ if (!incompatibleImprovements.equals(other.incompatibleImprovements)) return false;
if (exposeFields != other.exposeFields) return false;
if (treatDefaultMethodsAsBeanMembers != other.treatDefaultMethodsAsBeanMembers) return false;
if (exposureLevel != other.exposureLevel) return false;
+ if (!memberAccessPolicy.equals(other.memberAccessPolicy)) return false;
if (methodAppearanceFineTuner != other.methodAppearanceFineTuner) return false;
if (methodSorter != other.methodSorter) return false;
@@ -135,6 +152,21 @@
this.treatDefaultMethodsAsBeanMembers = treatDefaultMethodsAsBeanMembers;
}
+ public MemberAccessPolicy getMemberAccessPolicy() {
+ return memberAccessPolicy;
+ }
+
+ /**
+ * Sets the {@link MemberAccessPolicy}; default is {@link DefaultMemberAccessPolicy#getInstance(Version)}, which
+ * is not appropriate if template editors aren't trusted.
+ *
+ * @since 2.3.30
+ */
+ public void setMemberAccessPolicy(MemberAccessPolicy memberAccessPolicy) {
+ NullArgumentException.check(memberAccessPolicy);
+ this.memberAccessPolicy = memberAccessPolicy;
+ }
+
public MethodAppearanceFineTuner getMethodAppearanceFineTuner() {
return methodAppearanceFineTuner;
}
@@ -151,11 +183,19 @@
this.methodSorter = methodSorter;
}
+ /**
+ * Returns the normalized incompatible improvements.
+ */
+ public Version getIncompatibleImprovements() {
+ return incompatibleImprovements;
+ }
+
private static void removeClearedReferencesFromInstanceCache() {
- Reference clearedRef;
+ Reference<? extends ClassIntrospector> clearedRef;
while ((clearedRef = INSTANCE_CACHE_REF_QUEUE.poll()) != null) {
synchronized (INSTANCE_CACHE) {
- findClearedRef: for (Iterator it = INSTANCE_CACHE.values().iterator(); it.hasNext(); ) {
+ findClearedRef: for (Iterator<Reference<ClassIntrospector>> it = INSTANCE_CACHE.values().iterator();
+ it.hasNext(); ) {
if (it.next() == clearedRef) {
it.remove();
break findClearedRef;
@@ -173,7 +213,7 @@
}
/** For unit testing only */
- static Map getInstanceCache() {
+ static Map<ClassIntrospectorBuilder, Reference<ClassIntrospector>> getInstanceCache() {
return INSTANCE_CACHE;
}
@@ -187,12 +227,12 @@
// Instance can be cached.
ClassIntrospector instance;
synchronized (INSTANCE_CACHE) {
- Reference instanceRef = (Reference) INSTANCE_CACHE.get(this);
- instance = instanceRef != null ? (ClassIntrospector) instanceRef.get() : null;
+ Reference<ClassIntrospector> instanceRef = INSTANCE_CACHE.get(this);
+ instance = instanceRef != null ? instanceRef.get() : null;
if (instance == null) {
ClassIntrospectorBuilder thisClone = (ClassIntrospectorBuilder) clone(); // prevent any aliasing issues
instance = new ClassIntrospector(thisClone, new Object(), true, true);
- INSTANCE_CACHE.put(thisClone, new WeakReference(instance, INSTANCE_CACHE_REF_QUEUE));
+ INSTANCE_CACHE.put(thisClone, new WeakReference<ClassIntrospector>(instance, INSTANCE_CACHE_REF_QUEUE));
}
}
@@ -207,8 +247,4 @@
}
}
- public boolean isBugfixed() {
- return bugfixed;
- }
-
}
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java
new file mode 100644
index 0000000..3a1e0e6
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ClassMemberAccessPolicy.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Returned by {@link MemberAccessPolicy#forClass(Class)}. The idea is that {@link MemberAccessPolicy#forClass(Class)}
+ * is called once per class, and then the methods of the resulting {@link ClassMemberAccessPolicy} object will be
+ * called for each member of the class. This can speed up the process as the class-specific lookups will be done only
+ * once per class, not once per member.
+ *
+ * @since 2.3.30
+ */
+public interface ClassMemberAccessPolicy {
+ boolean isMethodExposed(Method method);
+ boolean isConstructorExposed(Constructor<?> constructor);
+ boolean isFieldExposed(Field field);
+}
diff --git a/src/main/java/freemarker/core/_Java6.java b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
similarity index 62%
copy from src/main/java/freemarker/core/_Java6.java
copy to src/main/java/freemarker/ext/beans/ConstructorMatcher.java
index cd03fb2..0f5548c 100644
--- a/src/main/java/freemarker/core/_Java6.java
+++ b/src/main/java/freemarker/ext/beans/ConstructorMatcher.java
@@ -16,20 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
-package freemarker.core;
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+package freemarker.ext.beans;
+
+import java.lang.reflect.Constructor;
/**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
+ * {@link MemberMatcher} for constructors.
+ *
+ * @since 2.3.30
*/
-public interface _Java6 {
+final class ConstructorMatcher extends MemberMatcher<Constructor<?>, ExecutableMemberSignature> {
+ @Override
+ protected ExecutableMemberSignature toMemberSignature(Constructor<?> member) {
+ return new ExecutableMemberSignature(member);
+ }
- void setRoundingMode(DecimalFormat df, RoundingMode roundingMode);
-
- void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator);
-
+ @Override
+ protected boolean matchInUpperBoundTypeSubtypes() {
+ return false;
+ }
}
diff --git a/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java
new file mode 100644
index 0000000..c27f662
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/DefaultMemberAccessPolicy.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import freemarker.ext.beans.MemberSelectorListMemberAccessPolicy.MemberSelector;
+import freemarker.template.Version;
+import freemarker.template._TemplateAPI;
+
+/**
+ * Member access policy, used to implement default behavior that's mostly compatible with pre-2.3.30 versions, but is
+ * somewhat safer; it still can't provide safety in practice, if you allow untrusted users to edit templates! Use
+ * {@link WhitelistMemberAccessPolicy} if you need stricter control.
+ *
+ * @since 2.3.30
+ */
+public final class DefaultMemberAccessPolicy implements MemberAccessPolicy {
+
+ private static final DefaultMemberAccessPolicy INSTANCE = new DefaultMemberAccessPolicy();
+
+ private final Set<Class<?>> whitelistRuleFinalClasses;
+ private final Set<Class<?>> whitelistRuleNonFinalClasses;
+ private final WhitelistMemberAccessPolicy whitelistMemberAccessPolicy;
+ private final BlacklistMemberAccessPolicy blacklistMemberAccessPolicy;
+
+ /**
+ * Returns the singleton that's compatible with the given incompatible improvements version.
+ */
+ public static DefaultMemberAccessPolicy getInstance(Version incompatibleImprovements) {
+ _TemplateAPI.checkVersionNotNullAndSupported(incompatibleImprovements);
+ // All breakpoints here must occur in ClassIntrospectorBuilder.normalizeIncompatibleImprovementsVersion!
+ // Though currently we don't have any.
+ return INSTANCE;
+ }
+
+ private DefaultMemberAccessPolicy() {
+ try {
+ ClassLoader classLoader = DefaultMemberAccessPolicy.class.getClassLoader();
+
+ whitelistRuleFinalClasses = new HashSet<Class<?>>();
+ whitelistRuleNonFinalClasses = new HashSet<Class<?>>();
+ Set<Class<?>> typesWithBlacklistUnlistedRule = new HashSet<Class<?>>();
+ List<MemberSelector> whitelistMemberSelectors = new ArrayList<MemberSelector>();
+ for (String line : loadMemberSelectorFileLines()) {
+ line = line.trim();
+ if (!MemberSelector.isIgnoredLine(line)) {
+ if (line.startsWith("@")) {
+ String[] lineParts = line.split("\\s+");
+ if (lineParts.length != 2) {
+ throw new IllegalStateException("Malformed @ line: " + line);
+ }
+ String typeName = lineParts[1];
+ Class<?> upperBoundType;
+ try {
+ upperBoundType = classLoader.loadClass(typeName);
+ } catch (ClassNotFoundException e) {
+ upperBoundType = null;
+ }
+ String rule = lineParts[0].substring(1);
+ if (rule.equals("whitelistPolicyIfAssignable")) {
+ if (upperBoundType != null) {
+ Set<Class<?>> targetSet =
+ (upperBoundType.getModifiers() & Modifier.FINAL) != 0
+ ? whitelistRuleFinalClasses
+ : whitelistRuleNonFinalClasses;
+ targetSet.add(upperBoundType);
+ }
+ } else if (rule.equals("blacklistUnlistedMembers")) {
+ if (upperBoundType != null) {
+ typesWithBlacklistUnlistedRule.add(upperBoundType);
+ }
+ } else {
+ throw new IllegalStateException("Unhandled rule: " + rule);
+ }
+ } else {
+ MemberSelector memberSelector =
+ MemberSelector.parse(line, classLoader);
+ Class<?> upperBoundType = memberSelector.getUpperBoundType();
+ if (upperBoundType != null) {
+ if (!whitelistRuleFinalClasses.contains(upperBoundType)
+ && !whitelistRuleNonFinalClasses.contains(upperBoundType)
+ && !typesWithBlacklistUnlistedRule.contains(upperBoundType)) {
+ throw new IllegalStateException("Type without rule: " + upperBoundType.getName());
+ }
+ // We always do the same, as "blacklistUnlistedMembers" is also defined via a whitelist:
+ whitelistMemberSelectors.add(memberSelector);
+ }
+ }
+ }
+ }
+
+ whitelistMemberAccessPolicy = new WhitelistMemberAccessPolicy(whitelistMemberSelectors);
+
+ // Generate blacklists based on the whitelist and the members of "blacklistUnlistedMembers" types:
+ List<MemberSelector> blacklistMemberSelectors = new ArrayList<MemberSelector>();
+ for (Class<?> blacklistUnlistedRuleType : typesWithBlacklistUnlistedRule) {
+ ClassMemberAccessPolicy classPolicy = whitelistMemberAccessPolicy.forClass(blacklistUnlistedRuleType);
+ for (Method method : blacklistUnlistedRuleType.getMethods()) {
+ if (!classPolicy.isMethodExposed(method)) {
+ blacklistMemberSelectors.add(new MemberSelector(blacklistUnlistedRuleType, method));
+ }
+ }
+ for (Constructor<?> constructor : blacklistUnlistedRuleType.getConstructors()) {
+ if (!classPolicy.isConstructorExposed(constructor)) {
+ blacklistMemberSelectors.add(new MemberSelector(blacklistUnlistedRuleType, constructor));
+ }
+ }
+ for (Field field : blacklistUnlistedRuleType.getFields()) {
+ if (!classPolicy.isFieldExposed(field)) {
+ blacklistMemberSelectors.add(new MemberSelector(blacklistUnlistedRuleType, field));
+ }
+ }
+ }
+ blacklistMemberAccessPolicy = new BlacklistMemberAccessPolicy(blacklistMemberSelectors);
+ } catch (Exception e) {
+ throw new IllegalStateException("Couldn't init " + this.getClass().getName() + " instance", e);
+ }
+ }
+
+ private static List<String> loadMemberSelectorFileLines() throws IOException {
+ List<String> whitelist = new ArrayList<String>();
+ BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(
+ DefaultMemberAccessPolicy.class.getResourceAsStream("DefaultMemberAccessPolicy-rules"),
+ "UTF-8"));
+ try {
+ String line;
+ while ((line = reader.readLine()) != null){
+ whitelist.add(line);
+ }
+ } finally {
+ reader.close();
+ }
+
+ return whitelist;
+ }
+
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ if (isTypeWithWhitelistRule(contextClass)) {
+ return whitelistMemberAccessPolicy.forClass(contextClass);
+ } else {
+ return blacklistMemberAccessPolicy.forClass(contextClass);
+ }
+ }
+
+ private boolean isTypeWithWhitelistRule(Class<?> contextClass) {
+ if (whitelistRuleFinalClasses.contains(contextClass)) {
+ return true;
+ }
+ for (Class<?> nonFinalClass : whitelistRuleNonFinalClasses) {
+ if (nonFinalClass.isAssignableFrom(contextClass)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java
new file mode 100644
index 0000000..dfba692
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/ExecutableMemberSignature.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Used as a key in a {@link Map} or {@link Set} of methods or constructors.
+ *
+ * @since 2.3.30
+ */
+final class ExecutableMemberSignature {
+ private final String name;
+ private final Class<?>[] args;
+
+ ExecutableMemberSignature(String name, Class<?>[] args) {
+ this.name = name;
+ this.args = args;
+ }
+
+ /**
+ * Uses the method name, and the parameter types.
+ */
+ ExecutableMemberSignature(Method method) {
+ this(method.getName(), method.getParameterTypes());
+ }
+
+ /**
+ * Doesn't use the constructor name, only the parameter types.
+ */
+ ExecutableMemberSignature(Constructor<?> constructor) {
+ this("<init>", constructor.getParameterTypes());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ExecutableMemberSignature) {
+ ExecutableMemberSignature ms = (ExecutableMemberSignature) o;
+ return ms.name.equals(name) && Arrays.equals(args, ms.args);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode() + args.length * 31;
+ }
+}
diff --git a/src/main/java/freemarker/core/_Java6.java b/src/main/java/freemarker/ext/beans/FieldMatcher.java
similarity index 64%
rename from src/main/java/freemarker/core/_Java6.java
rename to src/main/java/freemarker/ext/beans/FieldMatcher.java
index cd03fb2..179ea26 100644
--- a/src/main/java/freemarker/core/_Java6.java
+++ b/src/main/java/freemarker/ext/beans/FieldMatcher.java
@@ -16,20 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
-package freemarker.core;
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+package freemarker.ext.beans;
+
+import java.lang.reflect.Field;
/**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
+ * {@link MemberMatcher} for fields.
+ *
+ * @since 2.3.30
*/
-public interface _Java6 {
+final class FieldMatcher extends MemberMatcher<Field, String> {
+ @Override
+ protected String toMemberSignature(Field member) {
+ return member.getName();
+ }
- void setRoundingMode(DecimalFormat df, RoundingMode roundingMode);
-
- void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator);
-
+ @Override
+ protected boolean matchInUpperBoundTypeSubtypes() {
+ return true;
+ }
}
diff --git a/src/main/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicy.java
new file mode 100644
index 0000000..13b6c41
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicy.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import freemarker.template.utility.ClassUtil;
+
+/**
+ * Legacy blacklist based member access policy, used only to keep old behavior, as it can't provide meaningful safety.
+ * Do not use it if you allow untrusted users to edit templates! Use {@link WhitelistMemberAccessPolicy} then.
+ *
+ * @since 2.3.30
+ */
+public final class LegacyDefaultMemberAccessPolicy implements MemberAccessPolicy {
+
+ public static final LegacyDefaultMemberAccessPolicy INSTANCE = new LegacyDefaultMemberAccessPolicy();
+
+ private static final String UNSAFE_METHODS_PROPERTIES = "unsafeMethods.properties";
+ private static final Set<Method> UNSAFE_METHODS = createUnsafeMethodsSet();
+
+ private static Set<Method> createUnsafeMethodsSet() {
+ try {
+ Properties props = ClassUtil.loadProperties(BeansWrapper.class, UNSAFE_METHODS_PROPERTIES);
+ Set<Method> set = new HashSet<Method>(props.size() * 4 / 3, 1f);
+ for (Object key : props.keySet()) {
+ try {
+ set.add(parseMethodSpec((String) key));
+ } catch (ClassNotFoundException e) {
+ if (ClassIntrospector.DEVELOPMENT_MODE) {
+ throw e;
+ }
+ } catch (NoSuchMethodException e) {
+ if (ClassIntrospector.DEVELOPMENT_MODE) {
+ throw e;
+ }
+ }
+ }
+ return set;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not load unsafe method set", e);
+ }
+ }
+
+ private static Method parseMethodSpec(String methodSpec)
+ throws ClassNotFoundException,
+ NoSuchMethodException {
+ int brace = methodSpec.indexOf('(');
+ int dot = methodSpec.lastIndexOf('.', brace);
+ Class<?> clazz = ClassUtil.forName(methodSpec.substring(0, dot));
+ String methodName = methodSpec.substring(dot + 1, brace);
+ String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
+ StringTokenizer tok = new StringTokenizer(argSpec, ",");
+ int argcount = tok.countTokens();
+ Class<?>[] argTypes = new Class[argcount];
+ for (int i = 0; i < argcount; i++) {
+ String argClassName = tok.nextToken();
+ argTypes[i] = ClassUtil.resolveIfPrimitiveTypeName(argClassName);
+ if (argTypes[i] == null) {
+ argTypes[i] = ClassUtil.forName(argClassName);
+ }
+ }
+ return clazz.getMethod(methodName, argTypes);
+ }
+
+ private LegacyDefaultMemberAccessPolicy() {
+ }
+
+ public ClassMemberAccessPolicy forClass(Class<?> containingClass) {
+ return CLASS_MEMBER_ACCESS_POLICY_INSTANCE;
+ }
+
+ private static final BlacklistClassMemberAccessPolicy CLASS_MEMBER_ACCESS_POLICY_INSTANCE
+ = new BlacklistClassMemberAccessPolicy();
+ private static class BlacklistClassMemberAccessPolicy implements ClassMemberAccessPolicy {
+
+ public boolean isMethodExposed(Method method) {
+ return !UNSAFE_METHODS.contains(method);
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return true;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
new file mode 100644
index 0000000..400f1ce
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberAccessPolicy.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import freemarker.core.Environment;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapper;
+import freemarker.template.TemplateModel;
+
+/**
+ * Implement this to restrict what class members (methods, fields, constructors) are accessible from templates.
+ * Note, however, that {@link BeansWrapper} and its subclasses doesn't discover all members on the first place, and the
+ * {@link MemberAccessPolicy} just removes from that set of members, never adds to it. Practically speaking, it's the
+ * last filter in the chain.
+ *
+ * <p>{@link MemberAccessPolicy}-s meant to be used inside {@link ObjectWrapper}-s, and their existence is transparent
+ * for the rest of the system. The instance is usually set via
+ * {@link BeansWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)} (or if you use {@link DefaultObjectWrapper},
+ * with {@link DefaultObjectWrapperBuilder#setMemberAccessPolicy(MemberAccessPolicy)}).
+ *
+ * <p>As {@link BeansWrapper}, and its subclasses like {@link DefaultObjectWrapper}, only discover public
+ * members, it's pointless to whitelist non-public members. (Also, while public members declared in non-public classes
+ * are discovered by {@link BeansWrapper}, Java reflection will not allow accessing those normally, so generally it's
+ * not useful to whitelist those either.)
+ *
+ * <p>Note that if you add {@link TemplateModel}-s directly to the data-model, those are not wrapped by the
+ * {@link ObjectWrapper} (from {@link Environment#getObjectWrapper()}), and so the {@link MemberAccessPolicy} won't
+ * affect those.
+ *
+ * <p>The {@link MemberAccessPolicy} is only used during the class introspection phase (which discovers the members of a
+ * type, and decides if, and how will they be exposed to templates), and the result of that is cached. So, the speed of
+ * an {@link MemberAccessPolicy} implementation is usually not too important, as it won't play a role during template
+ * execution.
+ *
+ * <p>Implementations must be thread-safe, and instances generally should be singletons on JVM level. FreeMarker
+ * caches its class metadata in a global (static, JVM-scope) cache for shared use, and the {@link MemberAccessPolicy}
+ * used is part of the cache key. Thus {@link MemberAccessPolicy} instances used at different places in the JVM
+ * should be equal according to {@link Object#equals(Object)}, as far as they implement exactly the same policy. It's
+ * not recommended to override {@link Object#equals(Object)}; use singletons and the default
+ * {@link Object#equals(Object)} implementation if possible.
+ *
+ * @since 2.3.30
+ */
+public interface MemberAccessPolicy {
+ /**
+ * Returns the {@link ClassMemberAccessPolicy} that encapsulates the member access policy for a given class.
+ * {@link ClassMemberAccessPolicy} implementations need not be thread-safe. Because class introspection results are
+ * cached, and so this method is usually only called once for a given class, the {@link ClassMemberAccessPolicy}
+ * instances shouldn't be cached by the implementation of this method.
+ *
+ * @param contextClass
+ * The exact class of object from which members will be get in the templates.
+ */
+ ClassMemberAccessPolicy forClass(Class<?> contextClass);
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberMatcher.java b/src/main/java/freemarker/ext/beans/MemberMatcher.java
new file mode 100644
index 0000000..34ccc9c
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberMatcher.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.reflect.Member;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * For implementing a whitelist or blacklist of class members in {@link MemberAccessPolicy} implementations.
+ * A {@link MemberMatcher} filters by name and/or signature, but not by by visibility, as
+ * the visibility condition is orthogonal to the whitelist or blacklist content.
+ *
+ * @since 2.3.30
+ */
+abstract class MemberMatcher<M extends Member, S> {
+ private final Map<S, Types> signaturesToUpperBoundTypes = new HashMap<S, Types>();
+
+ private static class Types {
+ private final Set<Class<?>> set = new HashSet<Class<?>>();
+ private boolean containsInterfaces;
+ }
+
+ /**
+ * Returns the {@link Map} lookup key used to match the member.
+ */
+ protected abstract S toMemberSignature(M member);
+
+ protected abstract boolean matchInUpperBoundTypeSubtypes();
+
+ /**
+ * 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
+ && (matchInUpperBoundTypeSubtypes()
+ ? containsTypeOrSuperType(upperBoundTypes, contextClass)
+ : containsExactType(upperBoundTypes, contextClass));
+ }
+
+ private static boolean containsExactType(Types types, Class<?> c) {
+ if (c == null) {
+ return false;
+ }
+ return types.set.contains(c);
+ }
+
+ private static boolean containsTypeOrSuperType(Types types, Class<?> c) {
+ if (c == null) {
+ return false;
+ }
+
+ if (types.set.contains(c)) {
+ return true;
+ }
+ if (containsTypeOrSuperType(types, c.getSuperclass())) {
+ return true;
+ }
+ if (types.containsInterfaces) {
+ for (Class<?> anInterface : c.getInterfaces()) {
+ if (containsTypeOrSuperType(types, anInterface)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/MemberSelectorListMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/MemberSelectorListMemberAccessPolicy.java
new file mode 100644
index 0000000..e0af091
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MemberSelectorListMemberAccessPolicy.java
@@ -0,0 +1,464 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import freemarker.log.Logger;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * Superclass for member-selector-list-based member access policies, like {@link WhitelistMemberAccessPolicy}.
+ *
+ * <p>There are two ways you can add members to the member selector list:
+ * <ul>
+ * <li>Via a list of member selectors passed to the constructor
+ * <li>Via annotation (concrete type depends on subclass)
+ * </ul>
+ *
+ * <p>Members are identified with the following data (with the example of
+ * {@code com.example.MyClass.myMethod(int, int)}):
+ * <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>If a method or field is matched in the upper bound type, it will be automatically matched in all subtypes of that.
+ * It's called "upper bound" type, because the member will only be matched in classes that are {@code instanceof}
+ * the upper bound class. That restriction stands even if the member was inherited from another type (class or
+ * interface), and it wasn't even overridden in the upper bound type; the member won't be matched in the
+ * type where it was inherited from, if that type is more generic than the upper bound type.
+ *
+ * <p>The above inheritance rule doesn't apply to constructors. That's consistent with the fact constructors aren't
+ * inherited in Java (or pretty much any other language). So for example, if you add {@code com.example.A.A()} to
+ * the member selector list, and {@code B extends A}, then {@code com.example.B.B()} is still not matched by that list.
+ * If you want it to be matched, you have to add {@code com.example.B.B()} to list explicitly.
+ *
+ * <p>Note that the return type of methods aren't used in any way. If {@code myMethod(int, int)} has multiple variants
+ * with different return types (which is possible on the bytecode level) but the same parameter types, then all
+ * variants of it is matched, or none is. Similarly, the type of fields isn't used either, only the name of the field
+ * matters.
+ *
+ * @since 2.3.30
+ */
+public abstract class MemberSelectorListMemberAccessPolicy implements MemberAccessPolicy {
+ private static final Logger LOG = Logger.getLogger("freemarker.beans");
+
+ enum ListType {
+ /** Only matched members will be exposed. */
+ WHITELIST,
+ /** Matched members will not be exposed. */
+ BLACKLIST
+ }
+
+ private final ListType listType;
+ private final MethodMatcher methodMatcher;
+ private final ConstructorMatcher constructorMatcher;
+ private final FieldMatcher fieldMatcher;
+ private final Class<? extends Annotation> matchAnnotation;
+
+ /**
+ * A condition that matches some type members. See {@link MemberSelectorListMemberAccessPolicy} 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 (should end with ')'): "
+ + memberSelectorString);
+ }
+ String argsSpec = cleanedStr.substring(postMemberNameIdx + 1, cleanedStr.length() - 1);
+ StringTokenizer tok = new StringTokenizer(argsSpec, ",");
+ int argCount = tok.countTokens();
+ Class<?>[] argTypes = new Class[argCount];
+ for (int i = 0; i < argCount; i++) {
+ String argClassName = tok.nextToken();
+ int arrayDimensions = 0;
+ while (argClassName.endsWith("[]")) {
+ arrayDimensions++;
+ argClassName = argClassName.substring(0, argClassName.length() - 2);
+ }
+ Class<?> argClass;
+ Class<?> primArgClass = ClassUtil.resolveIfPrimitiveTypeName(argClassName);
+ if (primArgClass != null) {
+ argClass = primArgClass;
+ } else {
+ if (!isWellFormedClassName(argClassName)) {
+ throw new IllegalArgumentException(
+ "Malformed whitelist entry (malformed argument class name): " + memberSelectorString);
+ }
+ try {
+ argClass = classLoader.loadClass(argClassName);
+ } catch (ClassNotFoundException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ } catch (SecurityException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ }
+ }
+ argTypes[i] = ClassUtil.getArrayClass(argClass, arrayDimensions);
+ }
+ try {
+ return memberName.equals(upperBoundClass.getSimpleName())
+ ? new MemberSelector(upperBoundClass, upperBoundClass.getConstructor(argTypes))
+ : new MemberSelector(upperBoundClass, upperBoundClass.getMethod(memberName, argTypes));
+ } catch (NoSuchMethodException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ } catch (SecurityException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ }
+ } else {
+ try {
+ return new MemberSelector(upperBoundClass, upperBoundClass.getField(memberName));
+ } catch (NoSuchFieldException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ } catch (SecurityException e) {
+ return new MemberSelector(upperBoundClass, e, cleanedStr);
+ }
+ }
+ }
+
+ /**
+ * Convenience method to parse all member selectors in the collection (see {@link #parse(String, ClassLoader)}),
+ * while also filtering out blank and comment lines; see {@link #parse(String, ClassLoader)},
+ * and {@link #isIgnoredLine(String)}.
+ */
+ public static List<MemberSelector> parse(Collection<String> memberSelectors,
+ ClassLoader classLoader) {
+ List<MemberSelector> parsedMemberSelectors = new ArrayList<MemberSelector>(memberSelectors.size());
+ for (String memberSelector : memberSelectors) {
+ if (!isIgnoredLine(memberSelector)) {
+ parsedMemberSelectors.add(parse(memberSelector, classLoader));
+ }
+ }
+ return parsedMemberSelectors;
+ }
+
+ /**
+ * A line is ignored if it's blank or a comment. A line is be blank if it doesn't contain non-whitespace
+ * character. A line is a comment if it starts with {@code #}, or {@code //} (ignoring any amount of
+ * preceding whitespace).
+ */
+ public static boolean isIgnoredLine(String line) {
+ String trimmedLine = line.trim();
+ return trimmedLine.length() == 0 || trimmedLine.startsWith("#") || trimmedLine.startsWith("//");
+ }
+ }
+
+ /**
+ * @param memberSelectors
+ * List of member selectors; see {@link MemberSelectorListMemberAccessPolicy} class-level documentation for
+ * more.
+ * @param listType
+ * Decides the "color" of the list
+ * @param matchAnnotation
+ */
+ MemberSelectorListMemberAccessPolicy(
+ Collection<? extends MemberSelector> memberSelectors, ListType listType,
+ Class<? extends Annotation> matchAnnotation) {
+ this.listType = listType;
+ this.matchAnnotation = matchAnnotation;
+
+ methodMatcher = new MethodMatcher();
+ constructorMatcher = new ConstructorMatcher();
+ fieldMatcher = new FieldMatcher();
+ for (MemberSelector memberSelector : memberSelectors) {
+ Class<?> upperBoundClass = memberSelector.upperBoundType;
+ if (memberSelector.exception != null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Member selector ignored due to error: " + memberSelector.getExceptionMemberSelectorString(),
+ memberSelector.exception);
+ }
+ } else if (memberSelector.constructor != null) {
+ constructorMatcher.addMatching(upperBoundClass, memberSelector.constructor);
+ } else if (memberSelector.method != null) {
+ methodMatcher.addMatching(upperBoundClass, memberSelector.method);
+ } else if (memberSelector.field != null) {
+ fieldMatcher.addMatching(upperBoundClass, memberSelector.field);
+ } else {
+ throw new AssertionError();
+ }
+ }
+ }
+
+ public final ClassMemberAccessPolicy forClass(final Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return matchResultToIsExposedResult(
+ methodMatcher.matches(contextClass, method)
+ || matchAnnotation != null
+ && _MethodUtil.getInheritableAnnotation(contextClass, method, matchAnnotation)
+ != null);
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return matchResultToIsExposedResult(
+ constructorMatcher.matches(contextClass, constructor)
+ || matchAnnotation != null
+ && _MethodUtil.getInheritableAnnotation(contextClass, constructor, matchAnnotation)
+ != null);
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return matchResultToIsExposedResult(
+ fieldMatcher.matches(contextClass, field)
+ || matchAnnotation != null
+ && _MethodUtil.getInheritableAnnotation(contextClass, field, matchAnnotation)
+ != null);
+ }
+ };
+ }
+
+ private boolean matchResultToIsExposedResult(boolean matches) {
+ if (listType == ListType.WHITELIST) {
+ return matches;
+ }
+ if (listType == ListType.BLACKLIST) {
+ return !matches;
+ }
+ throw new AssertionError();
+ }
+
+ private static boolean isWellFormedClassName(String s) {
+ if (s.length() == 0) {
+ return false;
+ }
+ int identifierStartIdx = 0;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (i == identifierStartIdx) {
+ if (!Character.isJavaIdentifierStart(c)) {
+ return false;
+ }
+ } else if (c == '.' && i != s.length() - 1) {
+ identifierStartIdx = i + 1;
+ } else {
+ if (!Character.isJavaIdentifierPart(c)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private static boolean isWellFormedJavaIdentifier(String s) {
+ if (s.length() == 0) {
+ return false;
+ }
+ if (!Character.isJavaIdentifierStart(s.charAt(0))) {
+ return false;
+ }
+ for (int i = 1; i < s.length(); i++) {
+ if (!Character.isJavaIdentifierPart(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/MethodMatcher.java b/src/main/java/freemarker/ext/beans/MethodMatcher.java
new file mode 100644
index 0000000..b7a5b07
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/MethodMatcher.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.reflect.Method;
+
+/**
+ * {@link MemberMatcher} for methods.
+ *
+ * <p>The return type (and visibility) of the methods will be ignored, only the method name and its parameter types
+ * matter. (The {@link MemberAccessPolicy}, and even {@link BeansWrapper} itself will still filter by visibility, it's
+ * just not the duty of the {@link MemberMatcher}.)
+ *
+ * @since 2.3.30
+ */
+final class MethodMatcher extends MemberMatcher<Method, ExecutableMemberSignature> {
+ @Override
+ protected ExecutableMemberSignature toMemberSignature(Method member) {
+ return new ExecutableMemberSignature(member);
+ }
+
+ @Override
+ protected boolean matchInUpperBoundTypeSubtypes() {
+ return true;
+ }
+}
diff --git a/src/main/java/freemarker/ext/beans/StaticModel.java b/src/main/java/freemarker/ext/beans/StaticModel.java
index 28c84bb..fc7504d 100644
--- a/src/main/java/freemarker/ext/beans/StaticModel.java
+++ b/src/main/java/freemarker/ext/beans/StaticModel.java
@@ -42,11 +42,11 @@
*/
final class StaticModel implements TemplateHashModelEx {
private static final Logger LOG = Logger.getLogger("freemarker.beans");
- private final Class clazz;
+ private final Class<?> clazz;
private final BeansWrapper wrapper;
- private final Map map = new HashMap();
+ private final Map<String, Object> map = new HashMap<String, Object>();
- StaticModel(Class clazz, BeansWrapper wrapper) throws TemplateModelException {
+ StaticModel(Class<?> clazz, BeansWrapper wrapper) throws TemplateModelException {
this.clazz = clazz;
this.wrapper = wrapper;
populate();
@@ -65,7 +65,7 @@
// Non-final field; this must be evaluated on each call.
if (model instanceof Field) {
try {
- return wrapper.getOuterIdentity().wrap(((Field) model).get(null));
+ return wrapper.readField(null, (Field) model);
} catch (IllegalAccessException e) {
throw new TemplateModelException(
"Illegal access for field " + key + " of class " + clazz.getName());
@@ -107,31 +107,34 @@
}
Field[] fields = clazz.getFields();
- for (int i = 0; i < fields.length; ++i) {
- Field field = fields[i];
+ for (Field field : fields) {
int mod = field.getModifiers();
if (Modifier.isPublic(mod) && Modifier.isStatic(mod)) {
- if (Modifier.isFinal(mod))
+ if (Modifier.isFinal(mod)) {
try {
// public static final fields are evaluated once and
// stored in the map
- map.put(field.getName(), wrapper.getOuterIdentity().wrap(field.get(null)));
+ map.put(field.getName(), wrapper.readField(null, field));
} catch (IllegalAccessException e) {
// Intentionally ignored
- } else
+ }
+ } else {
// This is a special flagging value: Field in the map means
// that this is a non-final field, and it must be evaluated
// on each get() call.
map.put(field.getName(), field);
+ }
}
}
if (wrapper.getExposureLevel() < BeansWrapper.EXPOSE_PROPERTIES_ONLY) {
+ ClassMemberAccessPolicy classMemberAccessPolicy =
+ wrapper.getClassIntrospector().getClassMemberAccessPolicyIfNotIgnored(clazz);
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; ++i) {
Method method = methods[i];
int mod = method.getModifiers();
if (Modifier.isPublic(mod) && Modifier.isStatic(mod)
- && wrapper.getClassIntrospector().isAllowedToExpose(method)) {
+ && ClassIntrospector.isMethodExposed(classMemberAccessPolicy, method)) {
String name = method.getName();
Object obj = map.get(name);
if (obj instanceof Method) {
@@ -154,8 +157,8 @@
}
}
}
- for (Iterator entries = map.entrySet().iterator(); entries.hasNext(); ) {
- Map.Entry entry = (Map.Entry) entries.next();
+ for (Iterator<Map.Entry<String, Object>> entries = map.entrySet().iterator(); entries.hasNext(); ) {
+ Map.Entry<String, Object> entry = entries.next();
Object value = entry.getValue();
if (value instanceof Method) {
Method method = (Method) value;
diff --git a/src/main/java/freemarker/ext/beans/TemplateAccessible.java b/src/main/java/freemarker/ext/beans/TemplateAccessible.java
new file mode 100644
index 0000000..5e07873
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/TemplateAccessible.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.ObjectWrapper;
+
+/**
+ * Indicates that the the annotated member can be exposed to templates; if the annotated member will be actually
+ * exposed depends on the {@link ObjectWrapper} in use, and how that was configured. When used with
+ * {@link BeansWrapper} or its subclasses, most notably with {@link DefaultObjectWrapper}, and you also set the
+ * {@link MemberAccessPolicy} to a {@link WhitelistMemberAccessPolicy}, it will acts as if the members annotated with
+ * this are in the whitelist. Note that adding something to the whitelist doesn't necessary make it visible from
+ * templates; see {@link WhitelistMemberAccessPolicy} documentation.
+ *
+ * @since 2.3.30
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
+public @interface TemplateAccessible {
+}
diff --git a/src/main/java/freemarker/ext/beans/UnsafeMethods.java b/src/main/java/freemarker/ext/beans/UnsafeMethods.java
deleted file mode 100644
index 249a6c1..0000000
--- a/src/main/java/freemarker/ext/beans/UnsafeMethods.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package freemarker.ext.beans;
-
-import java.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Properties;
-import java.util.Set;
-import java.util.StringTokenizer;
-
-import freemarker.template.utility.ClassUtil;
-
-class UnsafeMethods {
-
- private static final String UNSAFE_METHODS_PROPERTIES = "unsafeMethods.properties";
- private static final Set UNSAFE_METHODS = createUnsafeMethodsSet();
-
- private UnsafeMethods() { }
-
- static boolean isUnsafeMethod(Method method) {
- return UNSAFE_METHODS.contains(method);
- }
-
- private static final Set createUnsafeMethodsSet() {
- try {
- Properties props = ClassUtil.loadProperties(BeansWrapper.class, UNSAFE_METHODS_PROPERTIES);
- Set set = new HashSet(props.size() * 4 / 3, 1f);
- Map primClasses = createPrimitiveClassesMap();
- for (Object key : props.keySet()) {
- try {
- set.add(parseMethodSpec((String) key, primClasses));
- } catch (ClassNotFoundException e) {
- if (ClassIntrospector.DEVELOPMENT_MODE) {
- throw e;
- }
- } catch (NoSuchMethodException e) {
- if (ClassIntrospector.DEVELOPMENT_MODE) {
- throw e;
- }
- }
- }
- return set;
- } catch (Exception e) {
- throw new RuntimeException("Could not load unsafe method set", e);
- }
- }
-
- private static Method parseMethodSpec(String methodSpec, Map primClasses)
- throws ClassNotFoundException,
- NoSuchMethodException {
- int brace = methodSpec.indexOf('(');
- int dot = methodSpec.lastIndexOf('.', brace);
- Class clazz = ClassUtil.forName(methodSpec.substring(0, dot));
- String methodName = methodSpec.substring(dot + 1, brace);
- String argSpec = methodSpec.substring(brace + 1, methodSpec.length() - 1);
- StringTokenizer tok = new StringTokenizer(argSpec, ",");
- int argcount = tok.countTokens();
- Class[] argTypes = new Class[argcount];
- for (int i = 0; i < argcount; i++) {
- String argClassName = tok.nextToken();
- argTypes[i] = (Class) primClasses.get(argClassName);
- if (argTypes[i] == null) {
- argTypes[i] = ClassUtil.forName(argClassName);
- }
- }
- return clazz.getMethod(methodName, argTypes);
- }
-
- private static Map createPrimitiveClassesMap() {
- Map map = new HashMap();
- map.put("boolean", Boolean.TYPE);
- map.put("byte", Byte.TYPE);
- map.put("char", Character.TYPE);
- map.put("short", Short.TYPE);
- map.put("int", Integer.TYPE);
- map.put("long", Long.TYPE);
- map.put("float", Float.TYPE);
- map.put("double", Double.TYPE);
- return map;
- }
-
-}
diff --git a/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
new file mode 100644
index 0000000..f024d37
--- /dev/null
+++ b/src/main/java/freemarker/ext/beans/WhitelistMemberAccessPolicy.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import java.util.Collection;
+
+import freemarker.template.ObjectWrapper;
+
+/**
+ * Whitelist-based member access policy, that is, only members that are matched by the listing will be exposed.
+ * Note that {@link BeansWrapper} and its subclasses doesn't discover all members on the first place, and the
+ * {@link MemberAccessPolicy} just removes from that set of members, never adds to it.
+ *
+ * <p>The whitelist content is usually application specific, and can be significant work to put together, but it's the
+ * only way you can achieve any practical safety when you don't fully trust the users who can edit templates.
+ *
+ * <p>See more about the rules at {@link MemberSelectorListMemberAccessPolicy}.
+ * {@link TemplateAccessible} annotation may be used to add members to the whitelist.
+ *
+ * <p>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.
+ *
+ * @since 2.3.30
+ */
+public class WhitelistMemberAccessPolicy extends MemberSelectorListMemberAccessPolicy {
+
+ /**
+ * @param memberSelectors
+ * List of member selectors; see {@link MemberSelectorListMemberAccessPolicy} class-level documentation for
+ * more.
+ */
+ public WhitelistMemberAccessPolicy(Collection<? extends MemberSelector> memberSelectors) {
+ super(memberSelectors, ListType.WHITELIST, TemplateAccessible.class);
+ }
+
+}
diff --git a/src/main/java/freemarker/ext/beans/_MethodUtil.java b/src/main/java/freemarker/ext/beans/_MethodUtil.java
index 782b944..9f743bc 100644
--- a/src/main/java/freemarker/ext/beans/_MethodUtil.java
+++ b/src/main/java/freemarker/ext/beans/_MethodUtil.java
@@ -18,7 +18,9 @@
*/
package freemarker.ext.beans;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
@@ -317,4 +319,143 @@
.toString();
}
+ /**
+ * Similar to {@link Method#getAnnotation(Class)}, but will also search the annotation in the implemented
+ * interfaces and in the ancestor classes.
+ */
+ public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Method method, Class<T> annotationClass) {
+ T result = method.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ return getInheritableMethodAnnotation(
+ contextClass, method.getName(), method.getParameterTypes(), true, annotationClass);
+ }
+
+ private static <T extends Annotation> T getInheritableMethodAnnotation(
+ Class<?> contextClass, String methodName, Class<?>[] methodParamTypes,
+ boolean skipCheckingDirectMethod,
+ Class<T> annotationClass) {
+ if (!skipCheckingDirectMethod) {
+ Method similarMethod;
+ try {
+ similarMethod = contextClass.getMethod(methodName, methodParamTypes);
+ } catch (NoSuchMethodException e) {
+ similarMethod = null;
+ }
+ if (similarMethod != null) {
+ T result = similarMethod.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ for (Class<?> anInterface : contextClass.getInterfaces()) {
+ if (!anInterface.getName().startsWith("java.")) {
+ Method similarInterfaceMethod;
+ try {
+ similarInterfaceMethod = anInterface.getMethod(methodName, methodParamTypes);
+ } catch (NoSuchMethodException e) {
+ similarInterfaceMethod = null;
+ }
+ if (similarInterfaceMethod != null) {
+ T result = similarInterfaceMethod.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ }
+ Class<?> superClass = contextClass.getSuperclass();
+ if (superClass == Object.class || superClass == null) {
+ return null;
+ }
+ return getInheritableMethodAnnotation(superClass, methodName, methodParamTypes, false, annotationClass);
+ }
+
+ /**
+ * Similar to {@link Constructor#getAnnotation(Class)}, but will also search the annotation in the implemented
+ * interfaces and in the ancestor classes.
+ */
+ public static <T extends Annotation> T getInheritableAnnotation(
+ Class<?> contextClass, Constructor<?> constructor, Class<T> annotationClass) {
+ T result = constructor.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+
+ Class<?>[] paramTypes = constructor.getParameterTypes();
+ while (true) {
+ contextClass = contextClass.getSuperclass();
+ if (contextClass == Object.class || contextClass == null) {
+ return null;
+ }
+ try {
+ constructor = contextClass.getConstructor(paramTypes);
+ } catch (NoSuchMethodException e) {
+ constructor = null;
+ }
+ if (constructor != null) {
+ result = constructor.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ }
+
+ /**
+ * Similar to {@link Field#getAnnotation(Class)}, but will also search the annotation in the implemented
+ * interfaces and in the ancestor classes.
+ */
+ public static <T extends Annotation> T getInheritableAnnotation(Class<?> contextClass, Field field, Class<T> annotationClass) {
+ T result = field.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ return getInheritableFieldAnnotation(
+ contextClass, field.getName(), true, annotationClass);
+ }
+
+ private static <T extends Annotation> T getInheritableFieldAnnotation(
+ Class<?> contextClass, String fieldName,
+ boolean skipCheckingDirectField,
+ Class<T> annotationClass) {
+ if (!skipCheckingDirectField) {
+ Field similarField;
+ try {
+ similarField = contextClass.getField(fieldName);
+ } catch (NoSuchFieldException e) {
+ similarField = null;
+ }
+ if (similarField != null) {
+ T result = similarField.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ for (Class<?> anInterface : contextClass.getInterfaces()) {
+ if (!anInterface.getName().startsWith("java.")) {
+ Field similarInterfaceField;
+ try {
+ similarInterfaceField = anInterface.getField(fieldName);
+ } catch (NoSuchFieldException e) {
+ similarInterfaceField = null;
+ }
+ if (similarInterfaceField != null) {
+ T result = similarInterfaceField.getAnnotation(annotationClass);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ }
+ Class<?> superClass = contextClass.getSuperclass();
+ if (superClass == Object.class || superClass == null) {
+ return null;
+ }
+ return getInheritableFieldAnnotation(superClass, fieldName, false, annotationClass);
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/freemarker/ext/jdom/NodeListModel.java b/src/main/java/freemarker/ext/jdom/NodeListModel.java
index bc39bef..ef44832 100644
--- a/src/main/java/freemarker/ext/jdom/NodeListModel.java
+++ b/src/main/java/freemarker/ext/jdom/NodeListModel.java
@@ -60,6 +60,7 @@
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
/**
* Provides a template for wrapping JDOM objects. It is capable of storing not only
@@ -1138,7 +1139,7 @@
throws Exception {
org.jdom.input.SAXBuilder builder = new org.jdom.input.SAXBuilder();
Document document = builder.build(System.in);
- SimpleHash model = new SimpleHash();
+ SimpleHash model = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
model.put("document", new NodeListModel(document));
FileReader fr = new FileReader(args[0]);
Template template = new Template(args[0], fr);
diff --git a/src/main/java/freemarker/ext/jsp/FreemarkerTag.java b/src/main/java/freemarker/ext/jsp/FreemarkerTag.java
index 837ce3f..da54f18 100644
--- a/src/main/java/freemarker/ext/jsp/FreemarkerTag.java
+++ b/src/main/java/freemarker/ext/jsp/FreemarkerTag.java
@@ -28,6 +28,7 @@
import javax.servlet.jsp.tagext.BodyTag;
import javax.servlet.jsp.tagext.Tag;
+import freemarker.template.Configuration;
import freemarker.template.SimpleHash;
import freemarker.template.Template;
@@ -35,7 +36,10 @@
* Simple implementation of JSP tag to allow use of FreeMarker templates in
* JSP. Inspired by similar class in Velocity template engine developed by
* <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
+ *
+ * @deprecated This feature is not supported anymore, also, it uses the deprecated default {@link Configuration}.
*/
+@Deprecated
public class FreemarkerTag implements BodyTag {
private Tag parent;
private BodyContent bodyContent;
diff --git a/src/main/java/freemarker/ext/jsp/JspContextModel.java b/src/main/java/freemarker/ext/jsp/JspContextModel.java
index 3c28468..bd824f0 100644
--- a/src/main/java/freemarker/ext/jsp/JspContextModel.java
+++ b/src/main/java/freemarker/ext/jsp/JspContextModel.java
@@ -26,6 +26,10 @@
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
+/**
+ * @Deprecated This is used by the deprecated {@link FreemarkerTag}.
+ */
+@Deprecated
class JspContextModel
implements
TemplateHashModel {
diff --git a/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java b/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java
index 6428dda..16d9eab 100644
--- a/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java
+++ b/src/main/java/freemarker/ext/servlet/AllHttpScopesHashModel.java
@@ -30,6 +30,7 @@
import freemarker.template.SimpleHash;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template.utility.NullArgumentException;
/**
* An extension of SimpleHash that looks up keys in the hash, then in the
@@ -51,13 +52,14 @@
/**
* Creates a new instance of AllHttpScopesHashModel for handling a single
* HTTP servlet request.
- * @param wrapper the object wrapper to use
+ * @param objectWrapper the object wrapper to use; not {@code null}.
* @param context the servlet context of the web application
* @param request the HTTP servlet request being processed
*/
- public AllHttpScopesHashModel(ObjectWrapper wrapper,
+ public AllHttpScopesHashModel(ObjectWrapper objectWrapper,
ServletContext context, HttpServletRequest request) {
- setObjectWrapper(wrapper);
+ super(objectWrapper);
+ NullArgumentException.check("wrapper", objectWrapper);
this.context = context;
this.request = request;
}
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index c169e9a..3f0031d 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -468,7 +468,10 @@
/** FreeMarker version 2.3.29 (an {@link #Configuration(Version) incompatible improvements break-point}) */
public static final Version VERSION_2_3_29 = new Version(2, 3, 29);
-
+
+ /** FreeMarker version 2.3.30 (an {@link #Configuration(Version) incompatible improvements break-point}) */
+ public static final Version VERSION_2_3_30 = new Version(2, 3, 30);
+
/** The default of {@link #getIncompatibleImprovements()}, currently {@link #VERSION_2_3_0}. */
public static final Version DEFAULT_INCOMPATIBLE_IMPROVEMENTS = Configuration.VERSION_2_3_0;
/** @deprecated Use {@link #DEFAULT_INCOMPATIBLE_IMPROVEMENTS} instead. */
diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java
index 4c5a39d..eac0198 100644
--- a/src/main/java/freemarker/template/DefaultObjectWrapper.java
+++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java
@@ -32,7 +32,10 @@
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperConfiguration;
+import freemarker.ext.beans.DefaultMemberAccessPolicy;
import freemarker.ext.beans.EnumerationModel;
+import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
+import freemarker.ext.beans.MemberAccessPolicy;
import freemarker.ext.dom.NodeModel;
import freemarker.log.Logger;
@@ -252,7 +255,9 @@
* Called for an object that isn't considered to be of a "basic" Java type, like for an application specific type,
* or for a W3C DOM node. In its default implementation, W3C {@link Node}-s will be wrapped as {@link NodeModel}-s
* (allows DOM tree traversal), Jython objects will be delegated to the {@code JythonWrapper}, others will be
- * wrapped using {@link BeansWrapper#wrap(Object)}.
+ * wrapped using {@link BeansWrapper#wrap(Object)}. Note that if {@link #getMemberAccessPolicy()} doesn't return
+ * a {@link DefaultMemberAccessPolicy} or {@link LegacyDefaultMemberAccessPolicy}, then Jython wrapper will be
+ * skipped for security reasons.
*
* <p>
* When you override this method, you should first decide if you want to wrap the object in a custom way (and if so
@@ -263,8 +268,12 @@
if (obj instanceof Node) {
return wrapDomNode(obj);
}
- if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
- return JYTHON_WRAPPER.wrap(obj);
+ MemberAccessPolicy memberAccessPolicy = getMemberAccessPolicy();
+ if (memberAccessPolicy instanceof DefaultMemberAccessPolicy
+ || memberAccessPolicy instanceof LegacyDefaultMemberAccessPolicy) {
+ if (JYTHON_WRAPPER != null && JYTHON_OBJ_CLASS.isInstance(obj)) {
+ return JYTHON_WRAPPER.wrap(obj);
+ }
}
return super.wrap(obj);
}
diff --git a/src/main/java/freemarker/template/SimpleList.java b/src/main/java/freemarker/template/SimpleList.java
index 414fd11..af9506c 100644
--- a/src/main/java/freemarker/template/SimpleList.java
+++ b/src/main/java/freemarker/template/SimpleList.java
@@ -25,7 +25,7 @@
*
* <p>This class is thread-safe.
*
- * @deprecated Use SimpleSequence instead.
+ * @deprecated Use {@link SimpleSequence} instead.
* @see SimpleSequence
*/
diff --git a/src/main/java/freemarker/template/SimpleSequence.java b/src/main/java/freemarker/template/SimpleSequence.java
index 24b44e1..cf5560f 100644
--- a/src/main/java/freemarker/template/SimpleSequence.java
+++ b/src/main/java/freemarker/template/SimpleSequence.java
@@ -91,7 +91,7 @@
* the default object wrapper set in
* {@link WrappingTemplateModel#setDefaultObjectWrapper(ObjectWrapper)}.
*
- * @deprecated Use {@link #SimpleSequence(Collection, ObjectWrapper)}.
+ * @deprecated Use {@link #SimpleSequence(int, ObjectWrapper)}.
*/
@Deprecated
public SimpleSequence(int capacity) {
@@ -264,6 +264,9 @@
}
private class SynchronizedSequence extends SimpleSequence {
+ private SynchronizedSequence() {
+ super(SimpleSequence.this.getObjectWrapper());
+ }
@Override
public void add(Object obj) {
@@ -292,6 +295,11 @@
return SimpleSequence.this.toList();
}
}
+
+ @Override
+ public SimpleSequence synchronizedWrapper() {
+ return this;
+ }
}
}
\ No newline at end of file
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 30227ca..8d1683f 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -52,7 +52,35 @@
public static final int VERSION_INT_2_3_27 = Configuration.VERSION_2_3_27.intValue();
public static final int VERSION_INT_2_3_28 = Configuration.VERSION_2_3_28.intValue();
public static final int VERSION_INT_2_3_29 = Configuration.VERSION_2_3_29.intValue();
+ public static final int VERSION_INT_2_3_30 = Configuration.VERSION_2_3_30.intValue();
public static final int VERSION_INT_2_4_0 = Version.intValueFor(2, 4, 0);
+
+ /**
+ * Kind of a dummy {@link ObjectWrapper} used at places where the internal code earlier used the
+ * {@link ObjectWrapper#DEFAULT_WRAPPER} singleton, because it wasn't supposed to wrap/unwrap anything with it;
+ * never use this {@link ObjectWrapper}r in situations where values of arbitrary types need to be wrapped!
+ * The typical situation is that we are using {@link SimpleSequence}, or {@link SimpleHash}, which always has an
+ * {@link ObjectWrapper} field, even if we don't care in the given situation, and so we didn't set it explicitly.
+ * The concern with the old way is that the {@link ObjectWrapper} set in the {@link Configuration} is possibly
+ * more restrictive than the default, so if the template author can somehow make FreeMarker wrap something with the
+ * default {@link ObjectWrapper}, then we got a security problem. So we try not to have that around, if possible.
+ * The obvious fix, and the better engineering would be just use a such {@link TemplateSequenceModel} or
+ * {@link TemplateHashModelEx2} implementation at those places, which doesn't have an {@link ObjectWrapper} (and
+ * doesn't have the overhead of said implementations either). But, some user code might casts the values it
+ * receives (as directive argument for example) to {@link SimpleSequence} or {@link SimpleHash}, instead of to
+ * {@link TemplateSequenceModel} or {@link TemplateHashModelEx2}. Such user code is wrong, but still, if it worked
+ * so far fine (especially as sequence/hash literals are implemented by these "Simple" classes), it's better if it
+ * keeps working when they upgrade to 2.3.30. Such user code will be still out of luck if it also tries to add items
+ * which are not handled by {@link SimpleObjectWrapper}, but such abuse is even more unlikely, and this is how far
+ * we could go with this backward compatibility hack.
+ *
+ * @since 2.3.30
+ */
+ public static final SimpleObjectWrapper SAFE_OBJECT_WRAPPER;
+ static {
+ SAFE_OBJECT_WRAPPER = new SimpleObjectWrapper(Configuration.VERSION_2_3_0);
+ SAFE_OBJECT_WRAPPER.writeProtect();
+ }
public static void checkVersionNotNullAndSupported(Version incompatibleImprovements) {
NullArgumentException.check("incompatibleImprovements", incompatibleImprovements);
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index ad19750..95fd3b8 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -21,8 +21,11 @@
import java.io.IOException;
import java.io.InputStream;
+import java.lang.reflect.Array;
import java.net.URL;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Properties;
import java.util.Set;
@@ -88,7 +91,39 @@
// Fall back to the defining class loader of the FreeMarker classes
return Class.forName(className);
}
-
+
+ private static final Map<String, Class<?>> PRIMITIVE_CLASSES_BY_NAME;
+ static {
+ PRIMITIVE_CLASSES_BY_NAME = new HashMap<String, Class<?>>();
+ PRIMITIVE_CLASSES_BY_NAME.put("boolean", boolean.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("byte", byte.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("char", char.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("short", short.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("int", int.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("long", long.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("float", float.class);
+ PRIMITIVE_CLASSES_BY_NAME.put("double", double.class);
+ }
+
+ /**
+ * Returns the {@link Class} for a primitive type name, or {@code null} if it's not the name of a primitive type.
+ *
+ * @since 2.3.30
+ */
+ public static Class<?> resolveIfPrimitiveTypeName(String typeName) {
+ return PRIMITIVE_CLASSES_BY_NAME.get(typeName);
+ }
+
+ /**
+ * Returns the array type that corresponds to the element type and the given number of array dimensions.
+ * If the dimension is 0, it just returns the element type as is.
+ *
+ * @since 2.3.30
+ */
+ public static Class<?> getArrayClass(Class<?> elementType, int dimensions) {
+ return dimensions == 0 ? elementType : Array.newInstance(elementType, new int[dimensions]).getClass();
+ }
+
/**
* Same as {@link #getShortClassName(Class, boolean) getShortClassName(pClass, false)}.
*
diff --git a/src/main/java/freemarker/template/utility/DOMNodeModel.java b/src/main/java/freemarker/template/utility/DOMNodeModel.java
index 5bb0b29..404fe45 100644
--- a/src/main/java/freemarker/template/utility/DOMNodeModel.java
+++ b/src/main/java/freemarker/template/utility/DOMNodeModel.java
@@ -38,6 +38,7 @@
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateSequenceModel;
+import freemarker.template._TemplateAPI;
/**
* A convenient wrapper class for wrapping a Node in the W3C DOM API.
@@ -72,7 +73,7 @@
if ("attributes".equals(key)) {
NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
- SimpleHash hash = new SimpleHash();
+ SimpleHash hash = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
for (int i = 0; i < attributes.getLength(); i++) {
Attr att = (Attr) attributes.item(i);
hash.put(att.getName(), att.getValue());
diff --git a/src/main/java/freemarker/template/utility/TemplateModelUtils.java b/src/main/java/freemarker/template/utility/TemplateModelUtils.java
index be38312..aaadb33 100644
--- a/src/main/java/freemarker/template/utility/TemplateModelUtils.java
+++ b/src/main/java/freemarker/template/utility/TemplateModelUtils.java
@@ -39,6 +39,7 @@
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
/**
* Static utility method related to {@link TemplateModel}-s that didn't fit elsewhere.
@@ -248,7 +249,7 @@
private void initKeys() throws TemplateModelException {
if (keys == null) {
Set<String> keySet = new HashSet<String>();
- SimpleSequence keySeq = new SimpleSequence((ObjectWrapper) null);
+ SimpleSequence keySeq = new SimpleSequence(_TemplateAPI.SAFE_OBJECT_WRAPPER);
for (TemplateHashModelEx hash : hashes) {
addKeys(keySet, keySeq, hash);
}
@@ -271,7 +272,7 @@
private void initValues() throws TemplateModelException {
if (values == null) {
- SimpleSequence seq = new SimpleSequence(size(), null);
+ SimpleSequence seq = new SimpleSequence(size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
// Note: size() invokes initKeys() if needed.
int ln = keys.size();
diff --git a/src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules b/src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules
new file mode 100644
index 0000000..48001c2
--- /dev/null
+++ b/src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules
@@ -0,0 +1,582 @@
+# 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.
+
+# Used by DefaultMemberAccessPolicy (not by LegacyDefaultMemberAccessPolicy).
+# It does NOT provide enough safety if template authors aren't as trusted as the developers; you need to use a custom
+# whitelist then (see WhitelistMemberAccessPolicy).
+
+# Each member entry must have a upper bound type that already has a rule defined. The rules are associated with the
+# upper bound type in the lines starting with @. The possible rules are:
+# - whitelistPolicyIfAssignable: Members of the type and of its subtypes can only access members that were whitelisted.
+# Thus, if you extend a such type, and add a new method, it won't be exposed, as it wasn't whitelisted.
+# - blacklistUnlistedMembers: Members of the type that are not listed will be blacklisted. Once a member was blacklisted,
+# it will be blacklisted in subtypes as well. If you extend a type that has tris rule, and add a new method, it will
+# be exposed, as it wasn't blacklisted.
+
+@blacklistUnlistedMembers java.lang.Object
+# Disallowed since 2.3.0: java.lang.Object.wait(long)
+# Disallowed since 2.3.0: java.lang.Object.wait(long,int)
+# Disallowed since 2.3.0: java.lang.Object.wait()
+java.lang.Object.equals(java.lang.Object)
+java.lang.Object.toString()
+java.lang.Object.hashCode()
+java.lang.Object.getClass()
+# Disallowed since 2.3.0: java.lang.Object.notify()
+# Disallowed since 2.3.0: java.lang.Object.notifyAll()
+
+@blacklistUnlistedMembers java.lang.Thread
+java.lang.Thread.getName()
+# Disallowed since 2.3.0, since 2.3.30 even when overridden: java.lang.Thread.run()
+java.lang.Thread.isInterrupted()
+# Disallowed since 2.3.30: java.lang.Thread.currentThread()
+# Disallowed since 2.3.30: java.lang.Thread.onSpinWait()
+# Disallowed since 2.3.0: java.lang.Thread.join(long,int)
+# Disallowed since 2.3.0: java.lang.Thread.join(long)
+# Disallowed since 2.3.0: java.lang.Thread.join()
+java.lang.Thread.getThreadGroup()
+# Disallowed since 2.3.0: java.lang.Thread.setContextClassLoader(java.lang.ClassLoader)
+java.lang.Thread.holdsLock(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.Thread.getStackTrace()
+java.lang.Thread.checkAccess()
+# Disallowed since 2.3.30: java.lang.Thread.dumpStack()
+# Disallowed since 2.3.0: java.lang.Thread.setPriority(int)
+# Disallowed since 2.3.0: java.lang.Thread.setDaemon(boolean)
+# Disallowed since 2.3.0: java.lang.Thread.start()
+# Disallowed since 2.3.0: java.lang.Thread.sleep(long)
+# Disallowed since 2.3.0: java.lang.Thread.sleep(long,int)
+java.lang.Thread.isDaemon()
+java.lang.Thread.getPriority()
+# Disallowed since 2.3.0: java.lang.Thread.getContextClassLoader()
+# Disallowed since 2.3.0: java.lang.Thread.resume()
+# Disallowed since 2.3.0: java.lang.Thread.interrupt()
+java.lang.Thread.activeCount()
+# Disallowed since 2.3.30: java.lang.Thread.enumerate(java.lang.Thread[])
+java.lang.Thread.isAlive()
+# Disallowed since 2.3.30: java.lang.Thread.setDefaultUncaughtExceptionHandler(java.lang.Thread$UncaughtExceptionHandler)
+# Disallowed since 2.3.30: java.lang.Thread.getUncaughtExceptionHandler()
+# Disallowed since 2.3.30: java.lang.Thread.yield()
+# Disallowed since 2.3.0: java.lang.Thread.stop()
+java.lang.Thread.interrupted()
+# Disallowed since 2.3.0: java.lang.Thread.suspend()
+# Disallowed since 2.3.0: java.lang.Thread.setName(java.lang.String)
+java.lang.Thread.countStackFrames()
+# Disallowed since 2.3.30: java.lang.Thread.getAllStackTraces()
+java.lang.Thread.getId()
+java.lang.Thread.getState()
+# Disallowed since 2.3.30: java.lang.Thread.getDefaultUncaughtExceptionHandler()
+# Disallowed since 2.3.30: java.lang.Thread.setUncaughtExceptionHandler(java.lang.Thread$UncaughtExceptionHandler)
+
+@whitelistPolicyIfAssignable java.lang.ThreadGroup
+java.lang.ThreadGroup.getName()
+# Disallowed since 2.3.30: java.lang.ThreadGroup.list()
+java.lang.ThreadGroup.getParent()
+java.lang.ThreadGroup.checkAccess()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.setDaemon(boolean)
+java.lang.ThreadGroup.isDaemon()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.resume()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.interrupt()
+java.lang.ThreadGroup.getMaxPriority()
+java.lang.ThreadGroup.activeCount()
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.ThreadGroup[],boolean)
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.ThreadGroup[])
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.Thread[])
+# Disallowed since 2.3.30: java.lang.ThreadGroup.enumerate(java.lang.Thread[],boolean)
+# Disallowed since 2.3.30: java.lang.ThreadGroup.uncaughtException(java.lang.Thread,java.lang.Throwable)
+# Disallowed since 2.3.0: java.lang.ThreadGroup.stop()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.suspend()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.setMaxPriority(int)
+java.lang.ThreadGroup.activeGroupCount()
+# Disallowed since 2.3.0: java.lang.ThreadGroup.destroy()
+java.lang.ThreadGroup.isDestroyed()
+java.lang.ThreadGroup.parentOf(java.lang.ThreadGroup)
+# Disallowed since 2.3.0: java.lang.ThreadGroup.allowThreadSuspension(boolean)
+
+@whitelistPolicyIfAssignable java.lang.Runtime
+# Disallowed since 2.3.30: java.lang.Runtime.getRuntime()
+# Disallowed since 2.3.0: java.lang.Runtime.exit(int)
+# Disallowed since 2.3.30: java.lang.Runtime.runFinalization()
+java.lang.Runtime.version()
+# Disallowed since 2.3.0: java.lang.Runtime.loadLibrary(java.lang.String)
+# Disallowed since 2.3.30: java.lang.Runtime.gc()
+# Disallowed since 2.3.0: java.lang.Runtime.load(java.lang.String)
+java.lang.Runtime.freeMemory()
+java.lang.Runtime.maxMemory()
+java.lang.Runtime.availableProcessors()
+# Disallowed since 2.3.0: java.lang.Runtime.halt(int)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String[])
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String,java.lang.String[],java.io.File)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String[],java.lang.String[])
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String[],java.lang.String[],java.io.File)
+# Disallowed since 2.3.0: java.lang.Runtime.exec(java.lang.String,java.lang.String[])
+# Disallowed since 2.3.0: java.lang.Runtime.addShutdownHook(java.lang.Thread)
+# Disallowed since 2.3.0: java.lang.Runtime.removeShutdownHook(java.lang.Thread)
+java.lang.Runtime.totalMemory()
+# Disallowed since 2.3.0: java.lang.Runtime.traceInstructions(boolean)
+# Disallowed since 2.3.0: java.lang.Runtime.traceMethodCalls(boolean)
+
+@whitelistPolicyIfAssignable java.lang.System
+# Disallowed since 2.3.0: java.lang.System.exit(int)
+# Disallowed since 2.3.0: java.lang.System.runFinalization()
+# Disallowed since 2.3.0: java.lang.System.runFinalizersOnExit(boolean)
+java.lang.System.getProperty(java.lang.String)
+java.lang.System.getProperty(java.lang.String,java.lang.String)
+java.lang.System.identityHashCode(java.lang.Object)
+java.lang.System.currentTimeMillis()
+java.lang.System.nanoTime()
+# Disallowed since 2.3.30: java.lang.System.arraycopy(java.lang.Object,int,java.lang.Object,int,int)
+# Disallowed since 2.3.30: java.lang.System.getSecurityManager()
+java.lang.System.mapLibraryName(java.lang.String)
+# Disallowed since 2.3.0: java.lang.System.loadLibrary(java.lang.String)
+# Disallowed since 2.3.30: java.lang.System.console()
+# Disallowed since 2.3.30: java.lang.System.inheritedChannel()
+# Disallowed since 2.3.0: java.lang.System.setSecurityManager(java.lang.SecurityManager)
+java.lang.System.lineSeparator()
+# Disallowed since 2.3.0: java.lang.System.setProperty(java.lang.String,java.lang.String)
+java.lang.System.getenv(java.lang.String)
+java.lang.System.getenv()
+# Disallowed since 2.3.30: java.lang.System.getLogger(java.lang.String,java.util.ResourceBundle)
+# Disallowed since 2.3.30: java.lang.System.getLogger(java.lang.String)
+# Disallowed since 2.3.30: java.lang.System.gc()
+# Disallowed since 2.3.0: java.lang.System.setIn(java.io.InputStream)
+# Disallowed since 2.3.0: java.lang.System.setOut(java.io.PrintStream)
+# Disallowed since 2.3.0: java.lang.System.setErr(java.io.PrintStream)
+java.lang.System.getProperties()
+# Disallowed since 2.3.0: java.lang.System.setProperties(java.util.Properties)
+# Disallowed since 2.3.0: java.lang.System.clearProperty(java.lang.String)
+# Disallowed since 2.3.0: java.lang.System.load(java.lang.String)
+
+@whitelistPolicyIfAssignable java.lang.ClassLoader
+java.lang.ClassLoader.getName()
+# Disallowed since 2.3.30: java.lang.ClassLoader.loadClass(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getPlatformClassLoader()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemClassLoader()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemResourceAsStream(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getResourceAsStream(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemResource(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getResource(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getResources(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getDefinedPackage(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.resources(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.isRegisteredAsParallelCapable()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getSystemResources(java.lang.String)
+# Disallowed since 2.3.30: java.lang.ClassLoader.getParent()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getUnnamedModule()
+# Disallowed since 2.3.30: java.lang.ClassLoader.getDefinedPackages()
+# Disallowed since 2.3.30: java.lang.ClassLoader.setDefaultAssertionStatus(boolean)
+# Disallowed since 2.3.30: java.lang.ClassLoader.setPackageAssertionStatus(java.lang.String,boolean)
+# Disallowed since 2.3.30: java.lang.ClassLoader.setClassAssertionStatus(java.lang.String,boolean)
+# Disallowed since 2.3.30: java.lang.ClassLoader.clearAssertionStatus()
+
+@whitelistPolicyIfAssignable java.security.ProtectionDomain
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getClassLoader()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getCodeSource()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.implies(java.security.Permission)
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getPermissions()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.getPrincipals()
+# Disallowed since 2.3.30: java.security.ProtectionDomain.staticPermissionsOnly()
+
+@whitelistPolicyIfAssignable java.lang.Class
+java.lang.Class.getName()
+# Disallowed since 2.3.30: java.lang.Class.forName(java.lang.Module,java.lang.String)
+# Disallowed since 2.3.0: java.lang.Class.forName(java.lang.String,boolean,java.lang.ClassLoader)
+# Disallowed since 2.3.0: java.lang.Class.forName(java.lang.String)
+# Disallowed since 2.3.30: java.lang.Class.getModule()
+java.lang.Class.getProtectionDomain()
+java.lang.Class.isAssignableFrom(java.lang.Class)
+java.lang.Class.isInstance(java.lang.Object)
+java.lang.Class.getModifiers()
+java.lang.Class.isInterface()
+java.lang.Class.isArray()
+java.lang.Class.isPrimitive()
+java.lang.Class.getSuperclass()
+java.lang.Class.cast(java.lang.Object)
+java.lang.Class.componentType()
+java.lang.Class.componentType()
+java.lang.Class.describeConstable()
+java.lang.Class.getComponentType()
+java.lang.Class.isAnnotation()
+java.lang.Class.isEnum()
+java.lang.Class.getTypeParameters()
+# Disallowed since 2.3.0: java.lang.Class.getClassLoader()
+# Disallowed since 2.3.0: java.lang.Class.newInstance()
+java.lang.Class.getInterfaces()
+java.lang.Class.getEnclosingClass()
+java.lang.Class.getSimpleName()
+java.lang.Class.getCanonicalName()
+# Disallowed since 2.3.30: java.lang.Class.getResourceAsStream(java.lang.String)
+# Disallowed since 2.3.30: java.lang.Class.getResource(java.lang.String)
+java.lang.Class.getPackageName()
+java.lang.Class.desiredAssertionStatus()
+java.lang.Class.getMethod(java.lang.String,java.lang.Class[])
+java.lang.Class.isAnnotationPresent(java.lang.Class)
+java.lang.Class.descriptorString()
+java.lang.Class.arrayType()
+java.lang.Class.toGenericString()
+java.lang.Class.isSynthetic()
+java.lang.Class.getGenericSuperclass()
+java.lang.Class.getPackage()
+java.lang.Class.getGenericInterfaces()
+# Disallowed since 2.3.30: java.lang.Class.getSigners()
+java.lang.Class.getEnclosingMethod()
+java.lang.Class.getEnclosingConstructor()
+java.lang.Class.getDeclaringClass()
+java.lang.Class.getTypeName()
+java.lang.Class.isAnonymousClass()
+java.lang.Class.isLocalClass()
+java.lang.Class.isMemberClass()
+java.lang.Class.getClasses()
+java.lang.Class.getFields()
+java.lang.Class.getMethods()
+java.lang.Class.getConstructors()
+java.lang.Class.getField(java.lang.String)
+java.lang.Class.getConstructor(java.lang.Class[])
+java.lang.Class.getDeclaredClasses()
+java.lang.Class.getDeclaredFields()
+java.lang.Class.getDeclaredMethods()
+java.lang.Class.getDeclaredConstructors()
+java.lang.Class.getDeclaredField(java.lang.String)
+java.lang.Class.getDeclaredMethod(java.lang.String,java.lang.Class[])
+java.lang.Class.getDeclaredConstructor(java.lang.Class[])
+java.lang.Class.getEnumConstants()
+java.lang.Class.asSubclass(java.lang.Class)
+java.lang.Class.getAnnotation(java.lang.Class)
+java.lang.Class.getAnnotationsByType(java.lang.Class)
+java.lang.Class.getAnnotations()
+java.lang.Class.getDeclaredAnnotation(java.lang.Class)
+java.lang.Class.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.Class.getDeclaredAnnotations()
+java.lang.Class.getAnnotatedSuperclass()
+java.lang.Class.getAnnotatedInterfaces()
+java.lang.Class.getNestHost()
+java.lang.Class.isNestmateOf(java.lang.Class)
+java.lang.Class.getNestMembers()
+
+@whitelistPolicyIfAssignable java.lang.Package
+java.lang.Package.getName()
+java.lang.Package.isAnnotationPresent(java.lang.Class)
+java.lang.Package.getPackage(java.lang.String)
+java.lang.Package.getAnnotation(java.lang.Class)
+java.lang.Package.getAnnotationsByType(java.lang.Class)
+java.lang.Package.getAnnotations()
+java.lang.Package.getDeclaredAnnotation(java.lang.Class)
+java.lang.Package.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.Package.getDeclaredAnnotations()
+java.lang.Package.getPackages()
+java.lang.Package.isSealed()
+java.lang.Package.isSealed(java.net.URL)
+java.lang.Package.getSpecificationTitle()
+java.lang.Package.getSpecificationVersion()
+java.lang.Package.getSpecificationVendor()
+java.lang.Package.getImplementationTitle()
+java.lang.Package.getImplementationVersion()
+java.lang.Package.getImplementationVendor()
+java.lang.Package.isCompatibleWith(java.lang.String)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Method
+# Disallowed since 2.3.0: java.lang.reflect.Method.invoke(java.lang.Object,java.lang.Object[])
+java.lang.reflect.Method.getName()
+java.lang.reflect.Method.getModifiers()
+java.lang.reflect.Method.getTypeParameters()
+java.lang.reflect.Method.getReturnType()
+java.lang.reflect.Method.getParameterTypes()
+java.lang.reflect.Method.toGenericString()
+java.lang.reflect.Method.isSynthetic()
+java.lang.reflect.Method.getDeclaringClass()
+java.lang.reflect.Method.getAnnotation(java.lang.Class)
+java.lang.reflect.Method.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.Method.setAccessible(boolean)
+java.lang.reflect.Method.isVarArgs()
+java.lang.reflect.Method.getParameterCount()
+java.lang.reflect.Method.getParameterAnnotations()
+java.lang.reflect.Method.getGenericParameterTypes()
+java.lang.reflect.Method.getGenericExceptionTypes()
+java.lang.reflect.Method.isDefault()
+java.lang.reflect.Method.getGenericReturnType()
+java.lang.reflect.Method.getExceptionTypes()
+java.lang.reflect.Method.isBridge()
+java.lang.reflect.Method.getDefaultValue()
+java.lang.reflect.Method.getAnnotatedReturnType()
+java.lang.reflect.Method.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Method.getAnnotatedParameterTypes()
+java.lang.reflect.Method.getParameters()
+java.lang.reflect.Method.getAnnotatedReceiverType()
+java.lang.reflect.Method.getAnnotatedExceptionTypes()
+java.lang.reflect.Method.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Method.getAnnotations()
+java.lang.reflect.Method.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Method.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Method.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Method.trySetAccessible()
+java.lang.reflect.Method.isAccessible()
+java.lang.reflect.Method.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Constructor
+java.lang.reflect.Constructor.getName()
+java.lang.reflect.Constructor.getModifiers()
+java.lang.reflect.Constructor.getTypeParameters()
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.newInstance(java.lang.Object[])
+java.lang.reflect.Constructor.getParameterTypes()
+java.lang.reflect.Constructor.toGenericString()
+java.lang.reflect.Constructor.isSynthetic()
+java.lang.reflect.Constructor.getDeclaringClass()
+java.lang.reflect.Constructor.getAnnotation(java.lang.Class)
+java.lang.reflect.Constructor.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.setAccessible(boolean)
+java.lang.reflect.Constructor.isVarArgs()
+java.lang.reflect.Constructor.getParameterCount()
+java.lang.reflect.Constructor.getParameterAnnotations()
+java.lang.reflect.Constructor.getGenericParameterTypes()
+java.lang.reflect.Constructor.getGenericExceptionTypes()
+java.lang.reflect.Constructor.getExceptionTypes()
+java.lang.reflect.Constructor.getAnnotatedReturnType()
+java.lang.reflect.Constructor.getAnnotatedReceiverType()
+java.lang.reflect.Constructor.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Constructor.getAnnotatedParameterTypes()
+java.lang.reflect.Constructor.getParameters()
+java.lang.reflect.Constructor.getAnnotatedExceptionTypes()
+java.lang.reflect.Constructor.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Constructor.getAnnotations()
+java.lang.reflect.Constructor.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Constructor.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Constructor.trySetAccessible()
+java.lang.reflect.Constructor.isAccessible()
+java.lang.reflect.Constructor.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Field
+java.lang.reflect.Field.getName()
+java.lang.reflect.Field.getModifiers()
+# Disallowed since 2.3.30: java.lang.reflect.Field.get(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getBoolean(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getByte(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getShort(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getChar(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getInt(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getLong(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getFloat(java.lang.Object)
+# Disallowed since 2.3.30: java.lang.reflect.Field.getDouble(java.lang.Object)
+java.lang.reflect.Field.toGenericString()
+java.lang.reflect.Field.isSynthetic()
+java.lang.reflect.Field.getDeclaringClass()
+java.lang.reflect.Field.getAnnotation(java.lang.Class)
+java.lang.reflect.Field.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Field.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.Field.set(java.lang.Object,java.lang.Object)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setAccessible(boolean)
+java.lang.reflect.Field.getGenericType()
+java.lang.reflect.Field.getType()
+# Disallowed since 2.3.0: java.lang.reflect.Field.setBoolean(java.lang.Object,boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setByte(java.lang.Object,byte)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setChar(java.lang.Object,char)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setShort(java.lang.Object,short)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setInt(java.lang.Object,int)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setLong(java.lang.Object,long)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setFloat(java.lang.Object,float)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setDouble(java.lang.Object,double)
+java.lang.reflect.Field.isEnumConstant()
+java.lang.reflect.Field.getAnnotatedType()
+java.lang.reflect.Field.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Field.getAnnotations()
+java.lang.reflect.Field.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Field.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Field.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Field.trySetAccessible()
+java.lang.reflect.Field.isAccessible()
+java.lang.reflect.Field.canAccess(java.lang.Object)
+
+@blacklistUnlistedMembers java.lang.reflect.AccessibleObject
+java.lang.reflect.AccessibleObject.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.AccessibleObject.getAnnotation(java.lang.Class)
+java.lang.reflect.AccessibleObject.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.AccessibleObject.getAnnotations()
+java.lang.reflect.AccessibleObject.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.AccessibleObject.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.AccessibleObject.getDeclaredAnnotations()
+# Disallowed since 2.3.0: java.lang.reflect.AccessibleObject.setAccessible(boolean)
+# Disallowed since 2.3.0: java.lang.reflect.AccessibleObject.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.30: java.lang.reflect.AccessibleObject.trySetAccessible()
+java.lang.reflect.AccessibleObject.isAccessible()
+java.lang.reflect.AccessibleObject.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.Member
+java.lang.reflect.Member.getName()
+java.lang.reflect.Member.getModifiers()
+java.lang.reflect.Member.isSynthetic()
+java.lang.reflect.Member.getDeclaringClass()
+
+@whitelistPolicyIfAssignable java.lang.reflect.GenericDeclaration
+java.lang.reflect.GenericDeclaration.getTypeParameters()
+java.lang.reflect.GenericDeclaration.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getAnnotation(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getAnnotations()
+java.lang.reflect.GenericDeclaration.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.GenericDeclaration.getDeclaredAnnotations()
+
+@whitelistPolicyIfAssignable java.lang.reflect.Executable
+java.lang.reflect.Executable.getName()
+java.lang.reflect.Executable.getModifiers()
+java.lang.reflect.Executable.getTypeParameters()
+java.lang.reflect.Executable.getParameterTypes()
+java.lang.reflect.Executable.toGenericString()
+java.lang.reflect.Executable.isSynthetic()
+java.lang.reflect.Executable.getDeclaringClass()
+java.lang.reflect.Executable.getAnnotation(java.lang.Class)
+java.lang.reflect.Executable.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Executable.getDeclaredAnnotations()
+java.lang.reflect.Executable.isVarArgs()
+java.lang.reflect.Executable.getAnnotatedParameterTypes()
+java.lang.reflect.Executable.getParameterCount()
+java.lang.reflect.Executable.getParameterAnnotations()
+java.lang.reflect.Executable.getGenericParameterTypes()
+java.lang.reflect.Executable.getGenericExceptionTypes()
+java.lang.reflect.Executable.getExceptionTypes()
+java.lang.reflect.Executable.getAnnotatedReturnType()
+java.lang.reflect.Executable.getParameters()
+java.lang.reflect.Executable.getAnnotatedReceiverType()
+java.lang.reflect.Executable.getAnnotatedExceptionTypes()
+java.lang.reflect.Executable.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.Executable.getAnnotations()
+java.lang.reflect.Executable.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Executable.getDeclaredAnnotationsByType(java.lang.Class)
+# Disallowed since 2.3.0: java.lang.reflect.Executable.setAccessible(boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Executable.setAccessible(java.lang.reflect.AccessibleObject[],boolean)
+# Disallowed since 2.3.0: java.lang.reflect.Executable.trySetAccessible()
+java.lang.reflect.Executable.isAccessible()
+java.lang.reflect.Executable.canAccess(java.lang.Object)
+
+@whitelistPolicyIfAssignable java.lang.reflect.TypeVariable
+java.lang.reflect.TypeVariable.getName()
+java.lang.reflect.TypeVariable.getBounds()
+java.lang.reflect.TypeVariable.getGenericDeclaration()
+java.lang.reflect.TypeVariable.getAnnotatedBounds()
+java.lang.reflect.TypeVariable.getTypeName()
+java.lang.reflect.TypeVariable.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.TypeVariable.getAnnotation(java.lang.Class)
+java.lang.reflect.TypeVariable.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.TypeVariable.getAnnotations()
+java.lang.reflect.TypeVariable.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.TypeVariable.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.TypeVariable.getDeclaredAnnotations()
+
+@whitelistPolicyIfAssignable java.lang.reflect.AnnotatedType
+java.lang.reflect.AnnotatedType.getType()
+java.lang.reflect.AnnotatedType.getAnnotatedOwnerType()
+java.lang.reflect.AnnotatedType.isAnnotationPresent(java.lang.Class)
+java.lang.reflect.AnnotatedType.getAnnotation(java.lang.Class)
+java.lang.reflect.AnnotatedType.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.AnnotatedType.getAnnotations()
+java.lang.reflect.AnnotatedType.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.AnnotatedType.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.AnnotatedType.getDeclaredAnnotations()
+
+@whitelistPolicyIfAssignable java.lang.reflect.Type
+java.lang.reflect.Type.getTypeName()
+
+@whitelistPolicyIfAssignable java.lang.reflect.Parameter
+java.lang.reflect.Parameter.getName()
+java.lang.reflect.Parameter.getModifiers()
+java.lang.reflect.Parameter.isSynthetic()
+java.lang.reflect.Parameter.getAnnotation(java.lang.Class)
+java.lang.reflect.Parameter.getAnnotationsByType(java.lang.Class)
+java.lang.reflect.Parameter.getAnnotations()
+java.lang.reflect.Parameter.getDeclaredAnnotation(java.lang.Class)
+java.lang.reflect.Parameter.getDeclaredAnnotationsByType(java.lang.Class)
+java.lang.reflect.Parameter.getDeclaredAnnotations()
+java.lang.reflect.Parameter.getType()
+java.lang.reflect.Parameter.getAnnotatedType()
+java.lang.reflect.Parameter.getParameterizedType()
+java.lang.reflect.Parameter.isVarArgs()
+java.lang.reflect.Parameter.isNamePresent()
+java.lang.reflect.Parameter.getDeclaringExecutable()
+java.lang.reflect.Parameter.isImplicit()
+java.lang.reflect.Parameter.isAnnotationPresent(java.lang.Class)
+
+@whitelistPolicyIfAssignable java.lang.annotation.Annotation
+java.lang.annotation.Annotation.annotationType()
+
+@whitelistPolicyIfAssignable java.lang.constant.ClassDesc
+java.lang.constant.ClassDesc.isArray()
+java.lang.constant.ClassDesc.isPrimitive()
+java.lang.constant.ClassDesc.componentType()
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.of(java.lang.String)
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.of(java.lang.String,java.lang.String)
+java.lang.constant.ClassDesc.packageName()
+java.lang.constant.ClassDesc.descriptorString()
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.ofDescriptor(java.lang.String)
+java.lang.constant.ClassDesc.arrayType()
+java.lang.constant.ClassDesc.arrayType()
+java.lang.constant.ClassDesc.arrayType(int)
+java.lang.constant.ClassDesc.displayName()
+java.lang.constant.ClassDesc.isClassOrInterface()
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.nested(java.lang.String,java.lang.String[])
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.nested(java.lang.String)
+# Disallowed since 2.3.30: java.lang.constant.ClassDesc.resolveConstantDesc(java.lang.invoke.MethodHandles$Lookup)
+
+@whitelistPolicyIfAssignable java.net.URL
+# Disallowed since 2.3.30: java.net.URL.openStream()
+java.net.URL.getHost()
+java.net.URL.getPort()
+java.net.URL.getDefaultPort()
+java.net.URL.sameFile(java.net.URL)
+java.net.URL.toExternalForm()
+# Disallowed since 2.3.30: java.net.URL.openConnection()
+# Disallowed since 2.3.30: java.net.URL.openConnection(java.net.Proxy)
+# Disallowed since 2.3.30: java.net.URL.getContent()
+# Disallowed since 2.3.30: java.net.URL.getContent(java.lang.Class[])
+java.net.URL.getProtocol()
+java.net.URL.getAuthority()
+java.net.URL.getFile()
+java.net.URL.getRef()
+java.net.URL.getQuery()
+java.net.URL.getPath()
+java.net.URL.getUserInfo()
+java.net.URL.toURI()
+# Disallowed since 2.3.30: java.net.URL.setURLStreamHandlerFactory(java.net.URLStreamHandlerFactory)
+
+@whitelistPolicyIfAssignable java.lang.constant.ClassDesc
+
+@whitelistPolicyIfAssignable java.net.URI
+java.net.URI.getRawAuthority()
+java.net.URI.compareTo(java.lang.Object)
+java.net.URI.compareTo(java.net.URI)
+java.net.URI.isAbsolute()
+java.net.URI.resolve(java.net.URI)
+java.net.URI.resolve(java.lang.String)
+java.net.URI.normalize()
+java.net.URI.getScheme()
+java.net.URI.isOpaque()
+java.net.URI.getRawFragment()
+java.net.URI.getRawQuery()
+java.net.URI.getRawPath()
+java.net.URI.getHost()
+java.net.URI.getPort()
+java.net.URI.create(java.lang.String)
+java.net.URI.getAuthority()
+java.net.URI.getQuery()
+java.net.URI.getPath()
+java.net.URI.getUserInfo()
+java.net.URI.toURL()
+java.net.URI.relativize(java.net.URI)
+java.net.URI.getRawSchemeSpecificPart()
+java.net.URI.parseServerAuthority()
+java.net.URI.getSchemeSpecificPart()
+java.net.URI.getRawUserInfo()
+java.net.URI.getFragment()
+java.net.URI.toASCIIString()
diff --git a/src/main/resources/freemarker/ext/beans/unsafeMethods.properties b/src/main/resources/freemarker/ext/beans/unsafeMethods.properties
index 05c1981..a8025af 100644
--- a/src/main/resources/freemarker/ext/beans/unsafeMethods.properties
+++ b/src/main/resources/freemarker/ext/beans/unsafeMethods.properties
@@ -15,6 +15,14 @@
# specific language governing permissions and limitations
# under the License.
+# Used by LegacyDefaultMemberAccessPolicy (not by DefaultMemberAccessPolicy).
+# It does NOT provide enough safety if template authors aren't as trusted as the developers; you need to use a custom
+# whitelist then (see WhitelistMemberAccessPolicy).
+
+# This is a blacklist, that is, methods mentioned here will be not be accessible, but everything else will be.
+# Furthermore, overridden version of the blacklisted methods will be accessible (which is strange, but we kept backward
+# compatibility).
+
java.lang.Object.wait()
java.lang.Object.wait(long)
java.lang.Object.wait(long,int)
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index c8a2aab..cc0bd39 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,10 +20,7 @@
<book conformance="docgen" version="5.0" xml:lang="en"
xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:ns5="http://www.w3.org/2000/svg"
- xmlns:ns4="http://www.w3.org/1998/Math/MathML"
- xmlns:ns3="http://www.w3.org/1999/xhtml"
- xmlns:ns="http://docbook.org/ns/docbook">
+>
<info>
<title>Apache FreeMarker Manual</title>
@@ -11194,9 +11191,8 @@
<para>The <literal>TemplateHashModel</literal> returned from
<literal>BeansWrapper.getEnumModels()</literal> can be used to
- create hash models for accessing values of enums on JRE 1.5 or
- later. (An attempt to invoke this method on an earlier JRE will
- result in an
+ create hash models for accessing values of enums. (An attempt to
+ invoke this method on an earlier JRE will result in an
<literal>UnsupportedOperationException</literal>.)</para>
<programlisting role="unspecified">BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
@@ -28887,10 +28883,11 @@
<answer>
<para>In general you shouldn't allow that, unless those users are
- system administrators or other trusted personnel. Consider
- templates as part of the source code just like
- <literal>*.java</literal> files are. If you still want to allow
- users to upload templates, here are what to consider:</para>
+ application developers, system administrators, or other highly
+ trusted personnel. Consider templates as part of the source code
+ just like <literal>*.java</literal> files are. If you still want
+ to allow users to upload templates, here's what to
+ consider:</para>
<itemizedlist>
<listitem>
@@ -28915,11 +28912,27 @@
<literal>List</literal>-s, <literal>Array</literal>-s,
<literal>String</literal>-s, <literal>Number</literal>-s,
<literal>Boolean</literal>-s and <literal>Date</literal>-s.
- For many application though that's too restrictive, and
- instead you need to implement your own extremely restrictive
- <literal>ObjectWrapper</literal>, which, for example, only
- exposes those members of POJO-s that were explicitly marked to
- be safe (opt-in approach).</para>
+ But for many application that's too restrictive, and instead
+ you have to create a
+ <literal>WhitelistMemberAccessPolicy</literal>, and create a
+ <literal>DefaultObjectWrapper</literal> (or other
+ <literal>BeansWrapper</literal> subclass that you would use)
+ that uses that. See the Java API documentation of
+ <literal>WhitelistMemberAccessPolicy</literal> for more. (Or,
+ you can roll your own <literal>MemberAccessPolicy</literal>
+ implementation, or even your own restrictive
+ <literal>ObjectWrapper</literal> implementation of
+ course.)</para>
+
+ <para>If you are creating <literal>TemplateModel</literal>-s
+ in custom code (instead of the
+ <literal>ObjectWrapper</literal> creating those), be sure you
+ avoid deprecated container constructors like <literal>new
+ SimpleSequence()</literal>, as those will use the also
+ deprecated default object wrapper instance, which doesn't have
+ the same restrictions than the
+ <literal>ObjectWrapper</literal> you have set on the
+ <literal>Configuration</literal>.</para>
<para>Also, don't forget about the <link
linkend="ref_buitin_api_and_has_api"><literal>?api</literal>
@@ -28935,16 +28948,22 @@
the <literal>ObjectWrapper</literal> is still in control, as
it decides what objects support <literal>?api</literal>, and
what will <literal>?api</literal> expose for them (it usually
- exposes the same as for a generic POJO).</para>
+ exposes the same as for a generic POJO). Members not allowed
+ by the <literal>MemberAccessPolicy</literal> also won't be
+ visible with <literal>?api</literal> (assuming you are using a
+ well behaving <literal>ObjectWrapper</literal>, like
+ <literal>DefaultObjectWrapper</literal> is, hopefully.)</para>
<para>Last not least, some maybe aware of that the standard
object wrappers filters out some well known
<quote>unsafe</quote> methods, like
- <literal>System.exit</literal>. Do not ever rely on this as
- your only line of defense, since it only blocks the methods
- that's in a predefined list. Thus, for example, if a new Java
- version adds a new problematic method, it won't be filtered
- out.</para>
+ <literal>System.exit</literal>. Do not ever rely on that,
+ since it only blocks the methods that's in a small predefined
+ list (for some historical reasons). The standard Java API is
+ huge and ever growing, and then there are the 3rd party
+ libraries, and the API-s of your own application. Clearly it's
+ impossible to blacklist all the problematic members in
+ those.</para>
</listitem>
<listitem>
@@ -28986,7 +29005,13 @@
<literal>TemplateClassResolver</literal> that restricts the
accessible classes (possibly based on which template asks for
them), such as
- <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.</para>
+ <literal>TemplateClassResolver.ALLOWS_NOTHING_RESOLVER</literal>.
+ Note that if, and only if your
+ <literal>ObjectWrapper</literal> is a
+ <literal>BeansWrapper</literal> or a subclass of it (typically
+ <literal>DefaultObjectWrapper</literal>), constructors not
+ allowed by the <literal>MemberAccessPolicy</literal> also
+ won't be accessible for <literal>?new</literal>.</para>
</listitem>
<listitem>
@@ -29186,6 +29211,63 @@
<itemizedlist>
<listitem>
+ <para><link
+ xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-124">FREEMARKER-124</link>:
+ Made the default filtering of class members more restrictive
+ (when you are using <literal>BeansWrapper</literal>, or its
+ subclasses like <literal>DefaultObjectWrapper</literal>). This
+ is not strictly backward compatible, but unlikely to break any
+ real-world applications; see
+ <literal>src/main/resources/freemarker/ext/beans/DefaultMemberAccessPolicy-rules</literal>
+ to see what was changed. This change was made for security
+ reasons, but the default behavior will never be safe enough if
+ untrusted users will edit templates; see <link
+ linkend="faq_template_uploading_security">in the FAQ</link>. In
+ the unlikely case this change breaks your application, then you
+ can still use the old behavior by setting the
+ <literal>memberAccessPolicy</literal> property of the object
+ wrapper to
+ <literal>LegacyDefaultMemberAccessPolicy.INSTANCE</literal>.</para>
+ </listitem>
+
+ <listitem>
+ <para>Added
+ <literal>freemarker.ext.beans.MemberAccessPolicy</literal>
+ interface, and the <literal>memberAccessPolicy</literal>
+ property to <literal>BeansWrapper</literal>, and subclasses like
+ <literal>DefaultObjectWrapper</literal>. This allows users to
+ implement their own program logic to decide what members of
+ classes will be exposed to the templates.</para>
+ </listitem>
+
+ <listitem>
+ <para>Added
+ <literal>freemarker.ext.beans.WhitelistMemberAccessPolicy</literal>,
+ which is a <literal>MemberAccessPolicy</literal> for use cases
+ where you want to allow editing templates to users who shouldn't
+ have the same rights as the developers (the same rights as the
+ Java application). Earlier, the only out of the box solution for
+ that was <literal>SimpleObjectWrapper</literal>, but that's too
+ restrictive for most applications where FreeMarker is used.
+ <literal>WhitelistMemberAccessPolicy</literal> works with
+ <literal>DefaultObjectWrapper</literal> (or any other
+ <literal>BeansWrapper</literal>), allowing you to use all
+ features of it, but it will only allow accessing members that
+ were explicitly listed by the developers, or was annotated with
+ <literal>@TemplateAccessible</literal>.</para>
+ </listitem>
+
+ <listitem>
+ <para><link
+ xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-120">FREEMARKER-120</link>:
+ <literal>BeansWrapper</literal> (and it's subclasses like
+ <literal>DefaultObjectWrapper</literal>) now has two protected
+ methods that can be overridden to monitor the accessing of
+ members: <literal>invokeMethod</literal> and
+ <literal>readField</literal>.</para>
+ </listitem>
+
+ <listitem>
<para>Added
<literal>Environment.getDataModelOrSharedVariable(String)</literal>.</para>
</listitem>
@@ -29200,7 +29282,7 @@
where the caller can provide the <literal>Map</literal> instance
used as the backing storage, thus allows controlling the
ordering, and other technical aspects (like the initial
- capacity) of the underlying <literal>Map</literal>.</para>
+ capacity) of it.</para>
</listitem>
</itemizedlist>
</section>
@@ -29299,6 +29381,11 @@
<itemizedlist>
<listitem>
+ <para>The minimum required Java version was increased from Java
+ 5 to Java 7.</para>
+ </listitem>
+
+ <listitem>
<para><link
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-109">FREEMARKER-109</link>:
In JSP TLD-s, line breaks inside function parameter lists have
diff --git a/src/test/java/freemarker/ext/beans/DefaultMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/DefaultMemberAccessPolicyTest.java
new file mode 100644
index 0000000..324abd1
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/DefaultMemberAccessPolicyTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.security.ProtectionDomain;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+
+public class DefaultMemberAccessPolicyTest {
+
+ private static final DefaultMemberAccessPolicy POLICY = DefaultMemberAccessPolicy.getInstance(Configuration.VERSION_2_3_30);
+
+ @Test
+ public void testWhitelistRuleWithNoMembers() throws NoSuchMethodException {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(ProtectionDomain.class);
+ assertFalse(classPolicy.isMethodExposed(ProtectionDomain.class.getMethod("getClassLoader")));
+ }
+
+ @Test
+ public void testWhitelistRuleWithSomeMembers() throws NoSuchMethodException {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(URL.class);
+ assertFalse(classPolicy.isMethodExposed(URL.class.getMethod("openStream")));
+ assertFalse(classPolicy.isMethodExposed(URL.class.getMethod("getContent", Class[].class)));
+ assertTrue(classPolicy.isMethodExposed(URL.class.getMethod("getHost")));
+ assertTrue(classPolicy.isMethodExposed(URL.class.getMethod("sameFile", URL.class)));
+ }
+
+ @Test
+ public void testWhitelistRuleOnSubclass() throws NoSuchMethodException {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(CustomClassLoader.class);
+ assertFalse(classPolicy.isMethodExposed(CustomClassLoader.class.getMethod("loadClass", String.class)));
+ assertFalse(classPolicy.isMethodExposed(CustomClassLoader.class.getMethod("m1")));
+ }
+
+ @Test
+ public void testBlacklistUnlistedRule() throws NoSuchMethodException {
+ for (Class<?> testedClass : Arrays.asList(
+ Object.class, Thread.class, ThreadSubclass.class, ProtectionDomain.class, CustomClassLoader.class,
+ UserClass.class)) {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(testedClass);
+ assertFalse(classPolicy.isMethodExposed(testedClass.getMethod("wait")));
+ assertTrue(testedClass.getName(), classPolicy.isMethodExposed(testedClass.getMethod("toString")));
+ }
+
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(UserClass.class);
+ assertTrue(classPolicy.isMethodExposed(UserClass.class.getMethod("foo")));
+ }
+
+ @Test
+ public void testBlacklistUnlistedRuleOnSubclass() throws NoSuchMethodException {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(ThreadSubclass.class);
+ assertFalse(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("run")));
+ assertTrue(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("getName")));
+ assertTrue(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("m1")));
+ }
+
+ @Test
+ public void testWellKnownUnsafeMethodsAreBanned() throws NoSuchMethodException {
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(Class.class);
+ assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("forName", String.class)));
+ assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("newInstance")));
+ assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getClassLoader")));
+ assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getResourceAsStream", String.class)));
+ assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getResource", String.class)));
+ assertTrue(classPolicy.isMethodExposed(Class.class.getMethod("getProtectionDomain"))); // Allowed
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(ProtectionDomain.class);
+ assertFalse(classPolicy.isMethodExposed(ProtectionDomain.class.getMethod("getClassLoader")));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(ClassLoader.class);
+ assertFalse(classPolicy.isMethodExposed(ClassLoader.class.getMethod("loadClass", String.class)));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(Method.class);
+ assertFalse(classPolicy.isMethodExposed(Method.class.getMethod("invoke", Object.class, Object[].class)));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(Constructor.class);
+ assertFalse(classPolicy.isMethodExposed(Constructor.class.getMethod("newInstance", Object[].class)));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(Field.class);
+ assertFalse(classPolicy.isMethodExposed(Field.class.getMethod("get", Object.class)));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(Object.class);
+ assertTrue(classPolicy.isMethodExposed(Field.class.getMethod("getClass"))); // Allowed by design
+ assertFalse(classPolicy.isMethodExposed(Field.class.getMethod("wait")));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = POLICY.forClass(URL.class);
+ assertFalse(classPolicy.isMethodExposed(URL.class.getMethod("openConnection")));
+ }
+ }
+
+ public static class ThreadSubclass extends Thread {
+ @Override
+ public void run() {
+ super.run();
+ }
+
+ public void m1() {}
+ }
+
+ public static class UserClass {
+ public void foo() {
+ }
+ }
+
+ public static class CustomClassLoader extends ClassLoader {
+ public void m1() {}
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
new file mode 100644
index 0000000..f9fcf47
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/DefaultObjectWrapperMemberAccessPolicyTest.java
@@ -0,0 +1,522 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.ObjectWrapperAndUnwrapper;
+import freemarker.template.SimpleNumber;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+
+public class DefaultObjectWrapperMemberAccessPolicyTest {
+
+ private final DefaultObjectWrapper dow
+ = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
+
+ @Test
+ public void testMethodsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ TemplateHashModel objM = (TemplateHashModel) dow.wrap(new C());
+
+ assertNotNull(objM.get("m1"));
+ assertEquals("m2(true)", exec(dow, objM.get("m2"), true));
+ assertEquals("staticM()", exec(dow, objM.get("staticM")));
+
+ assertEquals("x", getHashValue(dow, objM, "x"));
+ assertNotNull(objM.get("getX"));
+ assertNotNull(objM.get("setX"));
+
+ assertNull(objM.get("notPublic"));
+
+ assertNull(objM.get("notify"));
+
+ assertEquals("safe wait(1)", exec(dow, objM.get("wait"), 1L));
+ try {
+ exec(dow, objM.get("wait")); // 0 arg overload is not visible, a it's "unsafe"
+ fail();
+ } catch (TemplateModelException e) {
+ assertThat(e.getMessage(), containsString("wait(int)"));
+ }
+ }
+
+ @Test
+ public void testFieldsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ TemplateHashModel objM = (TemplateHashModel) dow.wrap(new C());
+ assertFieldsNotExposed(objM);
+ }
+
+ private void assertFieldsNotExposed(TemplateHashModel objM) throws TemplateModelException {
+ assertNull(objM.get("publicField1"));
+ assertNull(objM.get("publicField2"));
+ assertNonPublicFieldsNotExposed(objM);
+ }
+
+ private void assertNonPublicFieldsNotExposed(TemplateHashModel objM) throws TemplateModelException {
+ assertNull(objM.get("nonPublicField1"));
+ assertNull(objM.get("nonPublicField2"));
+
+ // Strangely, static fields are banned historically, while static methods aren't.
+ assertNull(objM.get("STATIC_FIELD"));
+ }
+
+ @Test
+ public void testGenericGetWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ TemplateHashModel objM = (TemplateHashModel) dow.wrap(new CWithGenericGet());
+
+ assertEquals("get(x)", getHashValue(dow, objM, "x"));
+ }
+
+ @Test
+ public void testBlacklistRuleWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ TemplateHashModel objM = (TemplateHashModel) dow.wrap(new CThread());
+
+ assertNull(getHashValue(dow, objM, "run")); // blacklisted in Thread
+ assertNotNull(getHashValue(dow, objM, "m1")); // As Thread doesn't use whitelisted rule
+ assertNotNull(getHashValue(dow, objM, "toString"));
+ }
+
+ @Test
+ public void testConstructorsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ assertNonPublicConstructorNotExposed(dow);
+
+ assertEquals(CWithConstructor.class, dow.newInstance(CWithConstructor.class, Collections.emptyList())
+ .getClass());
+
+ assertEquals(CWithOverloadedConstructor.class,
+ dow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList())
+ .getClass());
+
+ assertEquals(CWithOverloadedConstructor.class,
+ dow.newInstance(CWithOverloadedConstructor.class, Collections.singletonList(new SimpleNumber(1)))
+ .getClass());
+ }
+
+ private void assertNonPublicConstructorNotExposed(DefaultObjectWrapper ow) {
+ try {
+ ow.newInstance(C.class, Collections.emptyList());
+ fail();
+ } catch (TemplateModelException e) {
+ assertThat(e.getMessage(), containsString("constructor"));
+ }
+ }
+
+ @Test
+ public void testExposeAllWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setExposureLevel(DefaultObjectWrapper.EXPOSE_ALL);
+ DefaultObjectWrapper ow = owb.build();
+ TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+ // Because the MemberAccessPolicy is ignored:
+ assertNotNull(objM.get("notify"));
+ assertFieldsNotExposed(objM);
+ }
+
+ @Test
+ public void testExposeFieldsWithDefaultMemberAccessPolicy() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setExposeFields(true);
+ DefaultObjectWrapper ow = owb.build();
+ {
+ TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+ assertNull(objM.get("notify"));
+ assertEquals(1, getHashValue(ow, objM, "publicField1"));
+ assertEquals(2, getHashValue(ow, objM, "publicField2"));
+ assertNonPublicFieldsNotExposed(objM);
+ }
+
+ {
+ TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CExtended());
+ assertNull(objM.get("notify"));
+ assertEquals(1, getHashValue(ow, objM, "publicField1"));
+ assertEquals(2, getHashValue(ow, objM, "publicField2"));
+ assertEquals(3, getHashValue(ow, objM, "publicField3"));
+ assertNonPublicFieldsNotExposed(objM);
+ }
+ }
+
+ @Test
+ public void testMethodsWithCustomMemberAccessPolicy() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ String name = method.getName();
+ Class<?>[] paramTypes = method.getParameterTypes();
+ return name.equals("m3")
+ || (name.equals("m2")
+ && (paramTypes.length == 0 || paramTypes[0].equals(boolean.class)));
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return true;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+ assertNull(objM.get("m1"));
+ assertEquals("m3()", exec(ow, objM.get("m3")));
+ assertEquals("m2()", exec(ow, objM.get("m2")));
+ assertEquals("m2(true)", exec(ow, objM.get("m2"), true));
+ try {
+ exec(ow, objM.get("m2"), 1);
+ fail();
+ } catch (TemplateModelException e) {
+ assertThat(e.getMessage(), containsString("overload"));
+ }
+
+ assertNull(objM.get("notify"));
+ }
+
+ @Test
+ public void testFieldsWithCustomMemberAccessPolicy() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setExposeFields(true);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return true;
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return true;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return field.getName().equals("publicField1")
+ || field.getName().equals("nonPublicField1");
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ TemplateHashModel objM = (TemplateHashModel) ow.wrap(new C());
+
+ assertNonPublicFieldsNotExposed(objM);
+ assertEquals(1, getHashValue(ow, objM, "publicField1"));
+ assertNull(getHashValue(ow, objM, "publicField2"));
+ }
+
+ @Test
+ public void testGenericGetWithCustomMemberAccessPolicy() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return false;
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return true;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ TemplateHashModel objM = (TemplateHashModel) ow.wrap(new CWithGenericGet());
+ assertNull(getHashValue(ow, objM, "x"));
+ }
+
+ @Test
+ public void testConstructorsWithCustomMemberAccessPolicy() throws TemplateModelException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return true;
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return constructor.getDeclaringClass() == CWithOverloadedConstructor.class
+ && constructor.getParameterTypes().length == 1;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ assertNonPublicConstructorNotExposed(ow);
+
+ try {
+ assertEquals(CWithConstructor.class,
+ ow.newInstance(CWithConstructor.class, Collections.emptyList()).getClass());
+ fail();
+ } catch (TemplateModelException e) {
+ assertThat(e.getMessage(), containsString("constructor"));
+ }
+
+ try {
+ ow.newInstance(CWithOverloadedConstructor.class, Collections.emptyList());
+ fail();
+ } catch (TemplateModelException e) {
+ assertThat(e.getMessage(), containsString("constructor"));
+ }
+
+ assertEquals(CWithOverloadedConstructor.class,
+ ow.newInstance(CWithOverloadedConstructor.class,
+ Collections.singletonList(new SimpleNumber(1))).getClass());
+ }
+
+ @Test
+ public void testMemberAccessPolicyAndApiBI() throws IOException, TemplateException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return method.getName().equals("size");
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return true;
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ Map<String, Object> dataModel = ImmutableMap.<String, Object>of("m", ImmutableMap.of("k", "v"));
+
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setObjectWrapper(ow);
+ cfg.setAPIBuiltinEnabled(true);
+ Template template = new Template(null, "size=${m?api.size()} get=${(m?api.get('k'))!'hidden'}", cfg);
+
+ {
+ StringWriter out = new StringWriter();
+ template.process(dataModel, out);
+ assertEquals("size=1 get=hidden", out.toString());
+ }
+
+ cfg.setObjectWrapper(new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build());
+ {
+ StringWriter out = new StringWriter();
+ template.process(dataModel, out);
+ assertEquals("size=1 get=v", out.toString());
+ }
+ }
+
+ @Test
+ public void testMemberAccessPolicyAndNewBI() throws IOException, TemplateException, NoSuchMethodException {
+ DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ owb.setMemberAccessPolicy(new MemberAccessPolicy() {
+ public ClassMemberAccessPolicy forClass(Class<?> contextClass) {
+ return new ClassMemberAccessPolicy() {
+ public boolean isMethodExposed(Method method) {
+ return true;
+ }
+
+ public boolean isConstructorExposed(Constructor<?> constructor) {
+ return constructor.getDeclaringClass().equals(CustomModel.class);
+ }
+
+ public boolean isFieldExposed(Field field) {
+ return true;
+ }
+ };
+ }
+ });
+ DefaultObjectWrapper ow = owb.build();
+
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setObjectWrapper(ow);
+ cfg.setAPIBuiltinEnabled(true);
+ Template template = new Template(null,
+ "${'" + CustomModel.class.getName() + "'?new()} "
+ + "<#attempt>${'" + OtherCustomModel.class.getName() + "'?new()}<#recover>failed</#attempt>",
+ cfg);
+
+ {
+ StringWriter out = new StringWriter();
+ template.process(null, out);
+ assertEquals("1 failed", out.toString());
+ }
+
+ DefaultObjectWrapper dow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30).build();
+ MemberAccessPolicy pol = dow.getMemberAccessPolicy();
+ ClassMemberAccessPolicy cpol = pol.forClass(CustomModel.class);
+ assertTrue(cpol.isConstructorExposed(CustomModel.class.getConstructor()));
+ cfg.setObjectWrapper(dow);
+ {
+ StringWriter out = new StringWriter();
+ template.process(null, out);
+ assertEquals("1 2", out.toString());
+ }
+ }
+
+ private static Object getHashValue(ObjectWrapperAndUnwrapper ow, TemplateHashModel objM, String key)
+ throws TemplateModelException {
+ return ow.unwrap(objM.get(key));
+ }
+
+ private static Object exec(ObjectWrapperAndUnwrapper ow, TemplateModel objM, Object... args) throws TemplateModelException {
+ assertThat(objM, instanceOf(TemplateMethodModelEx.class));
+ List<TemplateModel> argModels = new ArrayList<TemplateModel>();
+ for (Object arg : args) {
+ argModels.add(ow.wrap(arg));
+ }
+ Object returnValue = ((TemplateMethodModelEx) objM).exec(argModels);
+ return unwrap(ow, returnValue);
+ }
+
+ private static Object unwrap(ObjectWrapperAndUnwrapper ow, Object returnValue) throws TemplateModelException {
+ return returnValue instanceof TemplateModel ? ow.unwrap((TemplateModel) returnValue) : returnValue;
+ }
+
+ public static class C {
+ public static final int STATIC_FIELD = 1;
+ public int publicField1 = 1;
+ public int publicField2 = 2;
+ protected int nonPublicField1 = 1;
+ private int nonPublicField2 = 2;
+
+ // Non-public
+ C() {
+
+ }
+
+ void notPublic() {
+ }
+
+ public void m1() {
+ }
+
+ public String m2() {
+ return "m2()";
+ }
+
+ public String m2(int otherOverload) {
+ return "m2(" + otherOverload + ")";
+ }
+
+ public String m2(boolean otherOverload) {
+ return "m2(" + otherOverload + ")";
+ }
+
+ public String m3() {
+ return "m3()";
+ }
+
+ public static String staticM() {
+ return "staticM()";
+ }
+
+ public String getX() {
+ return "x";
+ }
+
+ public void setX(String x) {
+ }
+
+ public String wait(int otherOverload) {
+ return "safe wait(" + otherOverload + ")";
+ }
+ }
+
+ public static class CExtended extends C {
+ public int publicField3 = 3;
+ }
+
+ public static class CWithGenericGet {
+ public String get(String key) {
+ return "get(" + key + ")";
+ }
+ }
+
+ public static class CWithConstructor implements TemplateModel {
+ public CWithConstructor() {
+ }
+ }
+
+ public static class CThread extends Thread {
+ @Override
+ public void run() {}
+
+ public void m1() {}
+ }
+
+ public static class CWithOverloadedConstructor implements TemplateModel {
+ public CWithOverloadedConstructor() {
+ }
+
+ public CWithOverloadedConstructor(int x) {
+ }
+ }
+
+ public static class CustomModel implements TemplateNumberModel {
+ public Number getAsNumber() {
+ return 1;
+ }
+ }
+
+ public static class OtherCustomModel implements TemplateNumberModel {
+ public Number getAsNumber() {
+ return 2;
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicyTest.java
new file mode 100644
index 0000000..e301ddf
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/LegacyDefaultMemberAccessPolicyTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class LegacyDefaultMemberAccessPolicyTest {
+
+ @Test
+ public void testBasic() throws NoSuchMethodException, NoSuchFieldException {
+ ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(UserClass.class);
+ assertFalse(classPolicy.isMethodExposed(UserClass.class.getMethod("wait")));
+ assertTrue(classPolicy.isMethodExposed(UserClass.class.getMethod("foo")));
+ assertTrue(classPolicy.isConstructorExposed(UserClass.class.getConstructor()));
+ assertTrue(classPolicy.isFieldExposed(UserClass.class.getField("f1")));
+ }
+
+ @Test
+ public void testThread() throws NoSuchMethodException, NoSuchFieldException {
+ {
+ ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(Thread.class);
+ assertFalse(classPolicy.isMethodExposed(Thread.class.getMethod("run")));
+ }
+ {
+ ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(ThreadSubclass.class);
+ // Strange glitch that we reproduce for backward compatibility:
+ assertTrue(classPolicy.isMethodExposed(ThreadSubclass.class.getMethod("run")));
+ }
+ }
+
+ @Test
+ public void testClass() throws NoSuchMethodException, NoSuchFieldException {
+ ClassMemberAccessPolicy classPolicy = LegacyDefaultMemberAccessPolicy.INSTANCE.forClass(Class.class);
+ assertFalse(classPolicy.isMethodExposed(Class.class.getMethod("getClassLoader")));
+ assertTrue(classPolicy.isMethodExposed(Class.class.getMethod("getName")));
+ }
+
+ public static class ThreadSubclass extends Thread {
+ @Override
+ public void run() {
+ super.run();
+ }
+
+ public void m1() {}
+ }
+
+ public static class UserClass {
+ public String f1;
+
+ public void foo() {
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MemberAccessMonitoringTest.java b/src/test/java/freemarker/ext/beans/MemberAccessMonitoringTest.java
new file mode 100644
index 0000000..b53d033
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MemberAccessMonitoringTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.test.TemplateTest;
+
+public class MemberAccessMonitoringTest extends TemplateTest {
+
+ private final MonitoredDefaultObjectWrapper ow = new MonitoredDefaultObjectWrapper();
+
+ @Override
+ protected Configuration createConfiguration() throws Exception {
+ Configuration configuration = super.createConfiguration();
+ configuration.setObjectWrapper(ow);
+ return configuration;
+ }
+
+ @Test
+ public void test() throws TemplateException, IOException {
+ addToDataModel("C1", ow.getStaticModels().get(C1.class.getName()));
+ addToDataModel("c2", new C2());
+
+ assertOutput(
+ "${C1.m1()} ${C1.F1} ${C1.F2} ${c2.m1()} ${c2.f1} ${c2.f2} ${c2['abc']}",
+ "1 11 111 2 22 222 3");
+ assertEquals(
+ ImmutableSet.of("C1.m1()", "C1.F1", "C1.F2", "C2.m1()", "C2.f1", "C2.f2", "C2.get()"),
+ ow.getAccessedMembers());
+ }
+
+ public static class C1 {
+ public static final int F1 = 11;
+ public static int F2 = 111;
+
+ public static int m1() {
+ return 1;
+ }
+
+ public static int get(String k) {
+ return k.length();
+ }
+ }
+
+ public static class C2 {
+ public final int f1 = 22;
+ public int f2 = 222;
+
+ public int m1() {
+ return 2;
+ }
+
+ public int get(String k) {
+ return k.length();
+ }
+ }
+
+ public static class MonitoredDefaultObjectWrapper extends DefaultObjectWrapper {
+ private final Set<String> accessedMembers;
+
+ public MonitoredDefaultObjectWrapper() {
+ super(getBuilder(), true);
+ this.accessedMembers = Collections.synchronizedSet(new HashSet<String>());
+ }
+
+ private static DefaultObjectWrapperBuilder getBuilder() {
+ DefaultObjectWrapperBuilder builder =
+ new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_30);
+ builder.setExposeFields(true);
+ return builder;
+ }
+
+ @Override
+ protected TemplateModel invokeMethod(Object object, Method method, Object[] args) throws
+ InvocationTargetException, IllegalAccessException, TemplateModelException {
+ accessedMembers.add(method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()");
+ return super.invokeMethod(object, method, args);
+ }
+
+ @Override
+ protected TemplateModel readField(Object object, Field field) throws IllegalAccessException,
+ TemplateModelException {
+ accessedMembers.add(field.getDeclaringClass().getSimpleName() + "." + field.getName());
+ return super.readField(object, field);
+ }
+
+ public Set<String> getAccessedMembers() {
+ return accessedMembers;
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MemberSelectorListAccessPolicyTest.java b/src/test/java/freemarker/ext/beans/MemberSelectorListAccessPolicyTest.java
new file mode 100644
index 0000000..1250c2a
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MemberSelectorListAccessPolicyTest.java
@@ -0,0 +1,614 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class MemberSelectorListAccessPolicyTest {
+
+ @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)));
+ assertFalse(c3Policy.isConstructorExposed(C3.class.getConstructor(int.class))); // Not inherited
+
+ 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 testBlacklist1() throws NoSuchMethodException, NoSuchFieldException {
+ BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
+ C1.class.getName() + ".m1()",
+ C1.class.getName() + ".f1",
+ C1.class.getName() + "." + C1.class.getSimpleName() + "()"
+ );
+
+ for (Class<?> cl : new Class[] { C1.class, C2.class, C3.class }) {
+ ClassMemberAccessPolicy classPolicy = policy.forClass(cl);
+ assertFalse(classPolicy.isMethodExposed(cl.getMethod("m1")));
+ assertTrue(classPolicy.isMethodExposed(cl.getMethod("m2", int.class)));
+ assertTrue(classPolicy.isMethodExposed(cl.getMethod("m3")));
+ assertFalse(classPolicy.isFieldExposed(cl.getField("f1")));
+ assertTrue(classPolicy.isFieldExposed(cl.getField("f2")));
+ if (cl != C2.class) {
+ assertEquals(cl != C1.class, classPolicy.isConstructorExposed(cl.getConstructor()));
+ }
+ assertTrue(classPolicy.isConstructorExposed(cl.getConstructor(int.class)));
+ }
+ }
+
+ @Test
+ public void testBlacklist2() throws NoSuchMethodException, NoSuchFieldException {
+ BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
+ C2.class.getName() + ".m1()",
+ C2.class.getName() + ".f1",
+ C2.class.getName() + "." + C2.class.getSimpleName() + "(int)"
+ );
+
+ {
+ Class<C1> lc = C1.class;
+ ClassMemberAccessPolicy classPolicy = policy.forClass(lc);
+ assertTrue(classPolicy.isMethodExposed(lc.getMethod("m1")));
+ assertTrue(classPolicy.isFieldExposed(lc.getField("f1")));
+ assertTrue(classPolicy.isConstructorExposed(lc.getConstructor(int.class)));
+ }
+
+ {
+ Class<C2> lc = C2.class;
+ ClassMemberAccessPolicy classPolicy = policy.forClass(lc);
+ assertFalse(classPolicy.isMethodExposed(lc.getMethod("m1")));
+ assertFalse(classPolicy.isFieldExposed(lc.getField("f1")));
+ assertFalse(classPolicy.isConstructorExposed(lc.getConstructor(int.class)));
+ }
+
+ {
+ Class<C3> lc = C3.class;
+ ClassMemberAccessPolicy classPolicy = policy.forClass(lc);
+ assertFalse(classPolicy.isMethodExposed(lc.getMethod("m1")));
+ assertFalse(classPolicy.isFieldExposed(lc.getField("f1")));
+ assertTrue(classPolicy.isConstructorExposed(lc.getConstructor(int.class)));
+ }
+ }
+
+ @Test
+ public void testBlacklistIgnoredAnnotation() throws NoSuchMethodException, NoSuchFieldException {
+ BlacklistMemberAccessPolicy policy = newBlacklistMemberAccessPolicy(
+ CAnnotationsTest1.class.getName() + ".m5()",
+ CAnnotationsTest1.class.getName() + ".f5",
+ CAnnotationsTest1.class.getName() + "." + CAnnotationsTest1.class.getSimpleName() + "()"
+ );
+
+ ClassMemberAccessPolicy classPolicy = policy.forClass(CAnnotationsTest1.class);
+ assertFalse(classPolicy.isMethodExposed(CAnnotationsTest1.class.getMethod("m5")));
+ assertFalse(classPolicy.isFieldExposed(CAnnotationsTest1.class.getField("f5")));
+ assertFalse(classPolicy.isConstructorExposed(CAnnotationsTest1.class.getConstructor()));
+ }
+
+ @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("should end with ')'"));
+ }
+ 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)));
+ }
+
+ private static WhitelistMemberAccessPolicy newWhitelistMemberAccessPolicy(String... memberSelectors) {
+ return new WhitelistMemberAccessPolicy(
+ MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+ Arrays.asList(memberSelectors),
+ MemberSelectorListAccessPolicyTest.class.getClassLoader()));
+ }
+
+ private static BlacklistMemberAccessPolicy newBlacklistMemberAccessPolicy(String... memberSelectors) {
+ return new BlacklistMemberAccessPolicy(
+ MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+ Arrays.asList(memberSelectors),
+ MemberSelectorListAccessPolicyTest.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);
+ }
+
+ @Override
+ public void m2(int x) {
+ }
+
+ public void m2(boolean x) {
+ }
+
+ public void m4() {
+ }
+ }
+
+ public static class C3 extends C2 {
+ public C3() {
+ super(0);
+ }
+
+ public C3(int x) {
+ super(x);
+ }
+ }
+
+ public static class D1 implements I1 {
+ public int f1;
+ public void m1() {
+ }
+ }
+
+ public static class D2 extends D1 {
+ }
+
+ public static class E1 implements I1Sub {
+ public void m1() {
+
+ }
+
+ public void m2() {
+ }
+ }
+
+ public static class E2 extends E1 implements J1 {
+ }
+
+ public static class F1 implements J1 {
+ public void m2() {
+ }
+ }
+
+ interface I1 {
+ int f1 = 1;
+ void m1();
+ }
+
+ interface I1Sub extends Serializable, I1 {
+ void m2();
+ }
+
+ interface J1 {
+ void m2();
+ }
+
+ public class CArrayArgs {
+ public void m1(String arg) {
+ }
+
+ public void m1(String[] arg) {
+ }
+
+ public void m1(String[][] arg) {
+ }
+
+ public void m2(C1[] arg) {
+ }
+
+ public void m2(C1[] arg1, C1[] arg2, C1 arg3) {
+ }
+ }
+
+ public static class CAnnotationsTest1 {
+ @TemplateAccessible
+ public int f5;
+
+ @TemplateAccessible
+ public CAnnotationsTest1() {}
+
+ @TemplateAccessible
+ public void m5() {}
+ }
+
+ public interface IAnnotationTest {
+ @TemplateAccessible
+ int f6 = 0;
+
+ @TemplateAccessible
+ void m6();
+ }
+
+ public static class CAnnotationsTest2 extends CAnnotationsTest1 implements IAnnotationTest {
+ public int f1;
+
+ public int f2;
+
+ @TemplateAccessible
+ public int f3;
+
+ @TemplateAccessible
+ public int f4;
+
+ public int f5;
+
+ public int f6;
+
+ public CAnnotationsTest2() {}
+
+ public CAnnotationsTest2(int x) {}
+
+ @TemplateAccessible
+ public CAnnotationsTest2(int x, int y) {}
+
+ @TemplateAccessible
+ public CAnnotationsTest2(int x, int y, int z) {}
+
+ public CAnnotationsTest2(int x, int y, int z, int a) {}
+
+ public void m1() {}
+
+ public void m2() {}
+
+ @TemplateAccessible
+ public void m3() {}
+
+ @TemplateAccessible
+ public void m4() {}
+
+ public void m5() {}
+
+ public void m6() {}
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MethodMatcherTest.java b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java
new file mode 100644
index 0000000..9ac9ca9
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodMatcherTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class MethodMatcherTest {
+
+ @Test
+ public void testReturnTypeOverload() throws NoSuchMethodException {
+ MethodMatcher matcher = new MethodMatcher();
+ Method genericM = TestReturnTypeOverloadGeneric.class.getMethod("m");
+ assertEquals(Object.class, genericM.getReturnType());
+ matcher.addMatching(TestReturnTypeOverloadGeneric.class, genericM);
+
+ Method stringM = TestReturnTypeOverloadString.class.getMethod("m");
+ assertEquals(String.class, stringM.getReturnType());
+
+ assertTrue(matcher.matches(TestReturnTypeOverloadGeneric.class, genericM));
+ assertTrue(matcher.matches(TestReturnTypeOverloadString.class, genericM));
+ assertTrue(matcher.matches(TestReturnTypeOverloadString.class, stringM));
+ }
+
+ public static class TestReturnTypeOverloadGeneric<T> {
+ public T m() {
+ return null;
+ };
+ }
+
+ public static class TestReturnTypeOverloadString extends TestReturnTypeOverloadGeneric<String> {
+ public String m() {
+ return "";
+ };
+ }
+
+ /** Mostly to test upper bound classes. */
+ @Test
+ public void testInheritance() throws NoSuchMethodException {
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC2.class.getMethod("m1");
+ assertEquals(m, TestInheritanceC1.class.getMethod("m1"));
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC2.class.getMethod("m2");
+ assertNotEquals(m, TestInheritanceC1.class.getMethod("m2"));
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ {
+ // m2 again, but with a non-same-instance but "equal" method.
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC1.class.getMethod("m2");
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = TestInheritanceC2.class.getMethod("m3");
+ assertEquals(m, TestInheritanceC1.class.getMethod("m3"));
+ assertNotEquals(m, TestInheritanceC3.class.getMethod("m3"));
+ matcher.addMatching(TestInheritanceC2.class, m);
+ assertFalse(matcher.matches(TestInheritanceC1.class, m));
+ assertTrue(matcher.matches(TestInheritanceC2.class, m));
+ assertTrue(matcher.matches(TestInheritanceC3.class, m));
+ }
+ }
+
+ public static class TestInheritanceC1 {
+ public void m1() {
+ }
+
+ public void m2() {
+ }
+
+ public void m3() {
+ }
+ }
+
+ public static class TestInheritanceC2 extends TestInheritanceC1 {
+ @Override
+ public void m2() {
+ }
+ }
+
+ public static class TestInheritanceC3 extends TestInheritanceC2 {
+ @Override
+ public void m3() {
+ }
+ }
+
+ /** Mostly to test when same method associated to multiple unrelated classes. */
+ @Test
+ public void testInheritance2() throws NoSuchMethodException {
+ MethodMatcher matcher = new MethodMatcher();
+ Method m = Runnable.class.getMethod("run");
+ matcher.addMatching(TestInheritance2SafeRunnable1.class, m);
+ matcher.addMatching(TestInheritance2SafeRunnable2.class, m);
+
+ assertTrue(matcher.matches(
+ TestInheritance2SafeRunnable1.class, TestInheritance2SafeRunnable1.class.getMethod("run")));
+ assertTrue(matcher.matches(
+ TestInheritance2SafeRunnable2.class, TestInheritance2SafeRunnable2.class.getMethod("run")));
+ assertFalse(matcher.matches(
+ TestInheritance2UnsafeRunnable.class, TestInheritance2UnsafeRunnable.class.getMethod("run")));
+ }
+
+ public static class TestInheritance2SafeRunnable1 implements Runnable {
+ public void run() {
+ }
+ }
+
+ public static class TestInheritance2SafeRunnable2 implements Runnable {
+ public void run() {
+ }
+ }
+
+ public static class TestInheritance2UnsafeRunnable implements Runnable {
+ public void run() {
+ }
+ }
+
+ @Test
+ public void testOverloads() throws NoSuchMethodException {
+ Method mInt = TestOverloads.class.getMethod("m", int.class);
+ Method mIntInt = TestOverloads.class.getMethod("m", int.class, int.class);
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ matcher.addMatching(TestOverloads.class, mInt);
+ assertTrue(matcher.matches(TestOverloads.class, mInt));
+ assertFalse(matcher.matches(TestOverloads.class, mIntInt));
+ }
+ {
+ MethodMatcher matcher = new MethodMatcher();
+ matcher.addMatching(TestOverloads.class, mIntInt);
+ assertFalse(matcher.matches(TestOverloads.class, mInt));
+ assertTrue(matcher.matches(TestOverloads.class, mIntInt));
+ }
+ }
+
+ public static class TestOverloads {
+ public void m(int x) {
+ }
+
+ public void m(int x, int y) {
+ }
+ }
+
+}
diff --git a/src/test/java/freemarker/ext/beans/MethodUtilTest.java b/src/test/java/freemarker/ext/beans/MethodUtilTest.java
new file mode 100644
index 0000000..24291b3
--- /dev/null
+++ b/src/test/java/freemarker/ext/beans/MethodUtilTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.ext.beans;
+
+import static org.junit.Assert.*;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import org.junit.Test;
+
+public class MethodUtilTest {
+
+ @Test
+ public void testMethodBasic() throws NoSuchMethodException, NoSuchFieldException {
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getMethod("m1"), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getMethod("m2"), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getConstructor(int.class), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getConstructor(int.class, int.class), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getField("f1"), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C1.class, C1.class.getField("f3"), TemplateAccessible.class));
+ }
+
+ @Test
+ public void testMethodInheritance() throws NoSuchMethodException, NoSuchFieldException {
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m1"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m2"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m3"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m4"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getMethod("m5"), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getConstructor(int.class), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getConstructor(), TemplateAccessible.class));
+
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f1"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f2"), TemplateAccessible.class));
+ assertNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f3"), TemplateAccessible.class));
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, C2.class.getField("f4"), TemplateAccessible.class));
+ }
+
+ @Test
+ public void testMethodInheritanceWithSyntheticMethod() {
+ for (Method method : D2.class.getMethods()) {
+ if (method.getName().equals("m1")) {
+ assertNotNull(_MethodUtil.getInheritableAnnotation(
+ C2.class, method, TemplateAccessible.class));
+ }
+ }
+ }
+
+ static public class C1 implements Serializable {
+ @TemplateAccessible
+ public int f1;
+
+ @TemplateAccessible
+ public int f2;
+
+ public int f3;
+
+ public int f4;
+
+ @TemplateAccessible
+ public C1(int x) {}
+
+ public C1(int x, int y) {}
+
+ @TemplateAccessible
+ public void m1() {}
+
+ public void m2() {}
+
+ public void m3() {}
+
+ @TemplateAccessible
+ public void m4() {}
+
+ @TemplateAccessible
+ public void m5() {}
+ }
+
+ static public class C2 extends C1 implements I1 {
+ public long f2;
+
+ public C2() {
+ super(0);
+ }
+
+ public C2(int x) {
+ super(x);
+ }
+
+ @Override
+ public void m1() {}
+
+ @TemplateAccessible
+ @Override
+ public void m3() {}
+ }
+
+ public interface I1 {
+ @TemplateAccessible
+ int f4 = 0;
+
+ @TemplateAccessible
+ void m2();
+
+ void m5();
+ }
+
+ public static class D1<T> {
+ @TemplateAccessible
+ public T m1() { return null; }
+ }
+
+ public static class D2 extends D1<String> {
+ @Override
+ public String m1() { return ""; }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index c18e47c..4b77eef 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -22,6 +22,7 @@
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
+import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
@@ -80,7 +81,11 @@
import freemarker.core.XMLOutputFormat;
import freemarker.core._CoreStringUtils;
import freemarker.ext.beans.BeansWrapperBuilder;
+import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
+import freemarker.ext.beans.MemberAccessPolicy;
+import freemarker.ext.beans.MemberSelectorListMemberAccessPolicy;
import freemarker.ext.beans.StringModel;
+import freemarker.ext.beans.WhitelistMemberAccessPolicy;
import freemarker.template.utility.DateUtil;
import freemarker.template.utility.NullArgumentException;
import freemarker.template.utility.NullWriter;
@@ -1896,6 +1901,39 @@
assertFalse(cfg.getFallbackOnNullLoopVariable());
}
+ public static final MemberAccessPolicy CONFIG_TEST_MEMBER_ACCESS_POLICY =
+ new WhitelistMemberAccessPolicy(MemberSelectorListMemberAccessPolicy.MemberSelector.parse(
+ ImmutableList.<String>of(
+ File.class.getName() + ".getName()",
+ File.class.getName() + ".isFile()"),
+ ConfigurationTest.class.getClassLoader()));
+
+ @Test
+ public void testMemberAccessPolicySetting() throws TemplateException {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setSetting(
+ "objectWrapper",
+ "DefaultObjectWrapper(2.3.30, "
+ + "memberAccessPolicy="
+ + ConfigurationTest.class.getName() + ".CONFIG_TEST_MEMBER_ACCESS_POLICY"
+ + ")");
+ TemplateHashModel m = (TemplateHashModel) cfg.getObjectWrapper().wrap(new File("x"));
+ assertNotNull(m.get("getName"));
+ assertNotNull(m.get("isFile"));
+ assertNull(m.get("delete"));
+ }
+
+ @Test
+ public void testMemberAccessPolicySetting2() throws TemplateException {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_30);
+ cfg.setSetting(
+ "objectWrapper",
+ "DefaultObjectWrapper(2.3.30, "
+ + "memberAccessPolicy=" + LegacyDefaultMemberAccessPolicy.class.getName() + "())");
+ assertSame(((DefaultObjectWrapper) cfg.getObjectWrapper()).getMemberAccessPolicy(),
+ LegacyDefaultMemberAccessPolicy.INSTANCE);
+ }
+
@Test
public void testGetSettingNamesAreSorted() throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
index 86d14bb..12f4954 100644
--- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
+++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java
@@ -27,6 +27,7 @@
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@@ -56,6 +57,7 @@
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.EnumerationModel;
import freemarker.ext.beans.HashAdapter;
+import freemarker.ext.beans.WhitelistMemberAccessPolicy;
import freemarker.ext.util.WrapperTemplateModel;
public class DefaultObjectWrapperTest {
@@ -316,6 +318,30 @@
assertTrue(bw.wrap(new PureIterable()) instanceof DefaultIterableAdapter);
}
+
+ {
+ DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.getVersion());
+
+ DefaultObjectWrapper bwDefault = builder.build();
+ assertSame(bwDefault, builder.build());
+
+ WhitelistMemberAccessPolicy memberAccessPolicy =
+ new WhitelistMemberAccessPolicy(
+ WhitelistMemberAccessPolicy.MemberSelector.parse(
+ Arrays.asList(SomeBean.class.getName() + ".getX()"),
+ DefaultObjectWrapperTest.class.getClassLoader()));
+ builder.setMemberAccessPolicy(memberAccessPolicy);
+ DefaultObjectWrapper bw = builder.build();
+ assertNotSame(bw, bwDefault);
+ assertSame(bw, builder.build());
+ assertSame(bw.getMemberAccessPolicy(), memberAccessPolicy);
+
+ TemplateHashModel m = (TemplateHashModel) bw.wrap(new SomeBean());
+ assertNotNull(m.get("x"));
+ assertNotNull(m.get("getX"));
+ assertNull(m.get("y"));
+ assertNull(m.get("getY"));
+ }
}
@Test
@@ -1191,5 +1217,14 @@
}
};
+
+ public static class SomeBean {
+ public int getX() {
+ return 1;
+ }
+ public int getY() {
+ return 1;
+ }
+ }
}
diff --git a/src/test/java/freemarker/test/templatesuite/models/LegacyList.java b/src/test/java/freemarker/test/templatesuite/models/LegacyList.java
index 06abaca..a02199b 100644
--- a/src/test/java/freemarker/test/templatesuite/models/LegacyList.java
+++ b/src/test/java/freemarker/test/templatesuite/models/LegacyList.java
@@ -24,6 +24,7 @@
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
+import freemarker.template._TemplateAPI;
/**
* A little bridge class that subclasses the new SimpleList
@@ -33,6 +34,10 @@
private Iterator iterator;
+ public LegacyList() {
+ super(_TemplateAPI.SAFE_OBJECT_WRAPPER);
+ }
+
/**
* Resets the cursor to the beginning of the list.
*/
diff --git a/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java b/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java
index f9ac0ca..09d8a71 100644
--- a/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java
+++ b/src/test/java/freemarker/test/templatesuite/models/MultiModel1.java
@@ -19,6 +19,9 @@
package freemarker.test.templatesuite.models;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.SimpleHash;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
@@ -34,19 +37,21 @@
public class MultiModel1 implements TemplateHashModel,
TemplateSequenceModel, TemplateScalarModel {
+ private static final DefaultObjectWrapper DEFAULT_OBJECT_WRAPPER =
+ new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_0).build();
private TemplateModel m_cSubModel = new MultiModel2();
private TemplateModel m_cListHashModel1 = new MultiModel4();
private TemplateModel m_cListHashModel2 = new MultiModel5();
- private TemplateSequenceModel m_cListModel = new SimpleSequence();
- private TemplateHashModel m_cHashModel = new SimpleHash();
+ private TemplateSequenceModel m_cListModel = new SimpleSequence(DEFAULT_OBJECT_WRAPPER);
+ private TemplateHashModel m_cHashModel = new SimpleHash(DEFAULT_OBJECT_WRAPPER);
/** Creates new MultiModel1 */
public MultiModel1() {
for ( int i = 0; i < 10; i++ ) {
((SimpleSequence) m_cListModel).add( "Model1 value: " + Integer.toString( i ));
}
- ((SimpleSequence) m_cListModel).add( new MultiModel3() );
- ((SimpleHash) m_cHashModel).put( "nested", new MultiModel3() );
+ ((SimpleSequence) m_cListModel).add(new MultiModel3());
+ ((SimpleHash) m_cHashModel).put("nested", new MultiModel3());
}
/**
diff --git a/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java b/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java
index f3de0a9..f317b18 100644
--- a/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java
+++ b/src/test/java/freemarker/test/templatesuite/models/TransformHashWrapper.java
@@ -24,6 +24,7 @@
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateScalarModel;
+import freemarker.template._TemplateAPI;
import freemarker.template.utility.HtmlEscape;
import freemarker.template.utility.StandardCompress;
@@ -33,7 +34,7 @@
public class TransformHashWrapper implements TemplateHashModel,
TemplateScalarModel {
- private SimpleHash m_cHashModel = new SimpleHash();
+ private SimpleHash m_cHashModel = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
/** Creates new TransformHashWrapper */
public TransformHashWrapper() {
diff --git a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl
index 9819fa0..583b444 100644
--- a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl
+++ b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl
@@ -1,3 +1,21 @@
+<#--
+ 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.
+-->
<#macro m1 a b c>
m1 does things with ${a}, ${b}, ${c}
</#macro>
diff --git a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out
index 54488c3..aa3a8ab 100644
--- a/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out
+++ b/src/test/resources/freemarker/manual/WithArgsExamples-usingWithArgsSpecialVariable.ftl.out
@@ -1,3 +1,21 @@
+/*
+ * 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.
+ */
m2 does things with 1, 2, 3
Delegate to m1:
diff --git a/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
index 4494b13..9cea85f 100644
--- a/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
+++ b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
@@ -1,3 +1,21 @@
+<#--
+ 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.
+-->
<#function f a b c d>
<#return "a=${a}, b=${b}, c=${c}, d=${d}">
</#function>
diff --git a/src/main/java/freemarker/core/_Java6.java b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl.out
similarity index 63%
copy from src/main/java/freemarker/core/_Java6.java
copy to src/test/resources/freemarker/manual/WithArgsLastExamples.ftl.out
index cd03fb2..c9d17b6 100644
--- a/src/main/java/freemarker/core/_Java6.java
+++ b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl.out
@@ -16,20 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
-package freemarker.core;
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
+a=2, b=3, c=1, d=2
+a=1, b=2, c=2, d=3
-/**
- * Used internally only, might changes without notice!
- * Used for accessing functionality that's only present in Java 6 or later.
- */
-public interface _Java6 {
+ a=1
+ b=2
+ others:
+ e = 5
+ f = 6
+ c = 3
+ d = 4
+ a=1
+ b=2
+ others:
+ c = 3
+ d = 4
+ e = 5
+ f = 6
- void setRoundingMode(DecimalFormat df, RoundingMode roundingMode);
-
- void setExponentSeparator(DecimalFormatSymbols dfs, String exponentSeparator);
-
-}
+ a = 1
+ b = 2
+ e = 5
+ f = 6
+ c = 3
+ d = 4
+ a = 1
+ b = 2
+ c = 3
+ d = 4
+ e = 5
+ f = 6