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;

Reply via email to