This is an automated email from the ASF dual-hosted git repository. ddekany pushed a commit to branch 2.3-gae in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit 5112b2a37c8ab500d740ba4597dfb6450e42e34d Author: ddekany <[email protected]> AuthorDate: Sun Jan 12 09:57:41 2020 +0100 FREEMARKER-120: BeansWrapper (and it's subclasses like DefaultObjectWrapper) now has two protected methods that can be overridden to monitor the accessing of members: invokeMethod and readField. --- src/main/java/freemarker/ext/beans/BeanModel.java | 2 +- .../java/freemarker/ext/beans/BeansWrapper.java | 48 ++++++-- .../java/freemarker/ext/beans/StaticModel.java | 4 +- src/manual/en_US/book.xml | 10 ++ .../ext/beans/MemberAccessMonitoringTest.java | 128 +++++++++++++++++++++ 5 files changed, 182 insertions(+), 10 deletions(-) diff --git a/src/main/java/freemarker/ext/beans/BeanModel.java b/src/main/java/freemarker/ext/beans/BeanModel.java index 6c68016..a6c5e4a 100644 --- a/src/main/java/freemarker/ext/beans/BeanModel.java +++ b/src/main/java/freemarker/ext/beans/BeanModel.java @@ -232,7 +232,7 @@ implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, Temp // 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/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java index d3fb070..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.beans.PropertyDescriptor; 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; @@ -1500,12 +1501,23 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { } } } - + /** - * 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. @@ -1516,9 +1528,13 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { * (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. @@ -1530,6 +1546,24 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { : 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 diff --git a/src/main/java/freemarker/ext/beans/StaticModel.java b/src/main/java/freemarker/ext/beans/StaticModel.java index 99e9329..fc7504d 100644 --- a/src/main/java/freemarker/ext/beans/StaticModel.java +++ b/src/main/java/freemarker/ext/beans/StaticModel.java @@ -65,7 +65,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 TemplateModelException( "Illegal access for field " + key + " of class " + clazz.getName()); @@ -114,7 +114,7 @@ final class StaticModel implements TemplateHashModelEx { 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 } diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 433ac9a..4d3ef5a 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -29259,6 +29259,16 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </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> 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; + } + } + +}
