This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/3 by this push:
new 7189aeb Forward ported from 2.3-gae: FREEMARKER-120:
DefaultObjectWrapper now has two protected methods that can be overridden to
monitor the accessing of members: invokeMethod and readField.
7189aeb is described below
commit 7189aeb8495e9396b36cc989e982950b4bc7067f
Author: ddekany <[email protected]>
AuthorDate: Sun Jan 12 10:23:45 2020 +0100
Forward ported from 2.3-gae: FREEMARKER-120: DefaultObjectWrapper now has
two protected methods that can be overridden to monitor the accessing of
members: invokeMethod and readField.
---
.../model/impl/MemberAccessMonitoringTest.java | 115 +++++++++++++++++++++
.../freemarker/core/model/impl/BeanModel.java | 2 +-
.../core/model/impl/ClassBasedModelFactory.java | 22 ++--
.../core/model/impl/DefaultObjectWrapper.java | 49 +++++++--
.../core/model/impl/MemberAccessPolicy.java | 5 +
.../freemarker/core/model/impl/StaticModel.java | 19 ++--
6 files changed, 181 insertions(+), 31 deletions(-)
diff --git
a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberAccessMonitoringTest.java
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberAccessMonitoringTest.java
new file mode 100644
index 0000000..ed19141
--- /dev/null
+++
b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/MemberAccessMonitoringTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import static org.junit.Assert.*;
+
+import java.io.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.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+public class MemberAccessMonitoringTest extends TemplateTest {
+
+ private MonitoredDefaultObjectWrapper ow = new
MonitoredDefaultObjectWrapper();
+
+ @Override
+ protected void
setupConfigurationBuilder(Configuration.ExtendableBuilder<?> cb) {
+ cb.setObjectWrapper(ow);
+ }
+
+ @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(new
DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0).exposeFields(true));
+ this.accessedMembers = Collections.synchronizedSet(new
HashSet<String>());
+ }
+
+ @Override
+ protected TemplateModel invokeMethod(Object object, Method method,
Object[] args) throws
+ InvocationTargetException, IllegalAccessException,
TemplateException {
+ accessedMembers.add(method.getDeclaringClass().getSimpleName() +
"." + method.getName() + "()");
+ return super.invokeMethod(object, method, args);
+ }
+
+ @Override
+ protected TemplateModel readField(Object object, Field field) throws
IllegalAccessException,
+ TemplateException {
+ accessedMembers.add(field.getDeclaringClass().getSimpleName() +
"." + field.getName());
+ return super.readField(object, field);
+ }
+
+ public Set<String> getAccessedMembers() {
+ return accessedMembers;
+ }
+ }
+
+}
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
index b0c99cb..585825b 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/BeanModel.java
@@ -181,7 +181,7 @@ public class BeanModel
// Unlike in FreeMarker 2, we always use the normal read method,
and ignore the indexed read method.
resultModel = wrapper.invokeMethod(object,
((FastPropertyDescriptor) desc).getReadMethod(), null);
} 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/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
index 087b1c7..d3d3207 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/ClassBasedModelFactory.java
@@ -36,8 +36,8 @@ import org.apache.freemarker.core.util._ClassUtils;
abstract class ClassBasedModelFactory implements TemplateHashModel {
private final DefaultObjectWrapper wrapper;
- private final Map/*<String,TemplateModel>*/ cache = new
ConcurrentHashMap();
- private final Set classIntrospectionsInProgress = new HashSet();
+ private final Map<String,TemplateModel> cache = new ConcurrentHashMap<>();
+ private final Set<String> classIntrospectionsInProgress = new HashSet<>();
protected ClassBasedModelFactory(DefaultObjectWrapper wrapper) {
this.wrapper = wrapper;
@@ -52,14 +52,14 @@ abstract class ClassBasedModelFactory implements
TemplateHashModel {
throw (TemplateException) e;
} else {
throw new TemplateException(e,
- "Failed to get valeu for key ", new
_DelayedJQuote(key), "; see cause exception.");
+ "Failed to get value for key ", new
_DelayedJQuote(key), "; see cause exception.");
}
}
}
private TemplateModel getInternal(String key) throws TemplateException,
ClassNotFoundException {
{
- TemplateModel model = (TemplateModel) cache.get(key);
+ TemplateModel model = cache.get(key);
if (model != null) return model;
}
@@ -67,7 +67,7 @@ abstract class ClassBasedModelFactory implements
TemplateHashModel {
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
@@ -76,10 +76,9 @@ abstract class ClassBasedModelFactory implements
TemplateHashModel {
// 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 +92,7 @@ abstract class ClassBasedModelFactory implements
TemplateHashModel {
classIntrospectorClearingCounter =
classIntrospector.getClearingCounter();
}
try {
- final Class clazz = _ClassUtils.forName(key);
+ final Class<?> clazz = _ClassUtils.forName(key);
// This is called so that we trigger the
// class-reloading detector. If clazz is a reloaded class,
@@ -129,14 +128,13 @@ abstract class ClassBasedModelFactory implements
TemplateHashModel {
}
}
- void removeFromCache(Class clazz) {
+ void removeFromCache(Class<?> clazz) {
synchronized (wrapper.getSharedIntrospectionLock()) {
cache.remove(clazz.getName());
}
}
- protected abstract TemplateModel createModel(Class clazz)
- throws TemplateException;
+ protected abstract TemplateModel createModel(Class<?> clazz) throws
TemplateException;
protected DefaultObjectWrapper getWrapper() {
return wrapper;
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
index 96a8655..6b31977 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/DefaultObjectWrapper.java
@@ -24,6 +24,7 @@ import java.lang.ref.WeakReference;
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;
@@ -970,10 +971,21 @@ public class DefaultObjectWrapper implements
RichObjectWrapper {
}
/**
- * 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, an empty string 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 TemplateStringModel#EMPTY_STRING} 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.
@@ -983,9 +995,9 @@ public class DefaultObjectWrapper implements
RichObjectWrapper {
* @throws TemplateException if the return value couldn't be wrapped
* (this can happen if the wrapper has an outer identity or is subclassed,
* and the outer identity or the subclass throws an exception. Plain
- * DefaultObjectWrapper never throws TemplateException).
+ * BeansWrapper never throws TemplateModelException).
*/
- TemplateModel invokeMethod(Object object, Method method, Object[] args)
+ protected TemplateModel invokeMethod(Object object, Method method,
Object[] args)
throws InvocationTargetException, IllegalAccessException,
TemplateException {
// [2.4]: Java's Method.invoke truncates numbers if the target type
has not enough bits to hold the value.
// There should at least be an option to check this.
@@ -997,6 +1009,22 @@ public class DefaultObjectWrapper implements
RichObjectWrapper {
}
/**
+ * 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[])
+ */
+ protected TemplateModel readField(Object object, Field field)
+ throws IllegalAccessException, TemplateException {
+ 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
* static methods on the specified class. To obtain a static model for a
@@ -1262,9 +1290,12 @@ public class DefaultObjectWrapper implements
RichObjectWrapper {
* {@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 Builder#setExposeFields(boolean)
expose_fields} and
- * {@link Builder#setExposureLevel(int) exposure_level}. So only
{@link DefaultObjectWrapper} where those
- * properties are the same may share class introspection caches among
each other.
+ * introspection:
+ * {@link Builder#setExposeFields(boolean) exposeFields}, and
+ * {@link Builder#setExposureLevel(int) exposureLevel}, and
+ * {@link Builder#setMemberAccessPolicy(MemberAccessPolicy)}
memberAccessPolicy}.
+ * So only {@link DefaultObjectWrapper} 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
DefaultObjectWrapper}. {@link Builder} returns the same
* {@link DefaultObjectWrapper} instance for equivalent properties
(unless the existing instance was garbage collected
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
index c00dbde..b603c82 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/MemberAccessPolicy.java
@@ -43,6 +43,11 @@ import org.apache.freemarker.core.model.TemplateModel;
* {@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
diff --git
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
index 83df66a..c586355 100644
---
a/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
+++
b/freemarker-core/src/main/java/org/apache/freemarker/core/model/impl/StaticModel.java
@@ -44,11 +44,11 @@ final class StaticModel implements TemplateHashModelEx {
private static final Logger LOG =
LoggerFactory.getLogger(StaticModel.class);
- private final Class clazz;
+ private final Class<?> clazz;
private final DefaultObjectWrapper wrapper;
- private final Map map = new HashMap();
+ private final Map<String, Object> map = new HashMap<>();
- StaticModel(Class clazz, DefaultObjectWrapper wrapper) throws
TemplateException {
+ StaticModel(Class<?> clazz, DefaultObjectWrapper wrapper) throws
TemplateException {
this.clazz = clazz;
this.wrapper = wrapper;
populate();
@@ -68,7 +68,7 @@ final class StaticModel implements TemplateHashModelEx {
// 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 TemplateException(
"Illegal access for field " + key + " of class " +
clazz.getName());
@@ -122,19 +122,20 @@ final class StaticModel implements TemplateHashModelEx {
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() <
DefaultObjectWrapper.EXPOSE_PROPERTIES_ONLY) {
@@ -167,8 +168,8 @@ final class StaticModel implements TemplateHashModelEx {
}
}
}
- 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;