Added new property to BeansWrapper.MethodAppearanceDecision: replaceExistingProperty. This is useful when a method like size() is exposed as a JavaBean property via MethodAppearanceDecision.exposeAsProperty, but there's also a real JavaBean property (like getSize()) with identical name. By default the real property isn't replaced, but now with replaceExistingProperty it can be.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/b349362c Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/b349362c Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/b349362c Branch: refs/heads/2.3 Commit: b349362cf07d4521e7d3a6ac665036a387ed8ad8 Parents: 2f24b4a Author: ddekany <ddek...@apache.org> Authored: Sun Jan 7 13:06:45 2018 +0100 Committer: ddekany <ddek...@apache.org> Committed: Sun Jan 7 13:06:45 2018 +0100 ---------------------------------------------------------------------- .../java/freemarker/ext/beans/BeansWrapper.java | 49 +++++++++++- .../freemarker/ext/beans/ClassIntrospector.java | 4 +- .../ext/beans/MethodAppearanceFineTuner.java | 13 +++- src/manual/en_US/book.xml | 40 ++++++++++ .../ext/beans/FineTuneMethodAppearanceTest.java | 78 +++++++++++++++++++- 5 files changed, 177 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b349362c/src/main/java/freemarker/ext/beans/BeansWrapper.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/ext/beans/BeansWrapper.java b/src/main/java/freemarker/ext/beans/BeansWrapper.java index 3dbb3e1..4b1ec6c 100644 --- a/src/main/java/freemarker/ext/beans/BeansWrapper.java +++ b/src/main/java/freemarker/ext/beans/BeansWrapper.java @@ -1766,35 +1766,82 @@ public class BeansWrapper implements RichObjectWrapper, WriteProtectable { */ static public final class MethodAppearanceDecision { private PropertyDescriptor exposeAsProperty; + private boolean replaceExistingProperty; private String exposeMethodAs; private boolean methodShadowsProperty; void setDefaults(Method m) { exposeAsProperty = null; + replaceExistingProperty = false; exposeMethodAs = m.getName(); methodShadowsProperty = true; } + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + */ public PropertyDescriptor getExposeAsProperty() { return exposeAsProperty; } + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + * Note that you may also want to call + * {@link #setMethodShadowsProperty(boolean) setMethodShadowsProperty(false)} when you call this. + */ public void setExposeAsProperty(PropertyDescriptor exposeAsProperty) { this.exposeAsProperty = exposeAsProperty; } - + + /** + * Getter pair of {@link #setReplaceExistingProperty(boolean)}. + * + * @since 2.3.28 + */ + public boolean getReplaceExistingProperty() { + return replaceExistingProperty; + } + + /** + * If {@link #getExposeAsProperty()} is non-{@code null}, and a {@link PropertyDescriptor} with the same + * property name was already added to the class introspection data, this decides if that will be replaced + * with the {@link PropertyDescriptor} returned by {@link #getExposeAsProperty()}. The default is {@code false}, + * that is, the old {@link PropertyDescriptor} is kept, and the new one is ignored. + * JavaBean properties discovered with the standard (non-{@link MethodAppearanceFineTuner}) mechanism + * are added before those created by the {@link MethodAppearanceFineTuner}, so with this you can decide if a + * real JavaBeans property can be replaced by the "fake" one created with + * {@link #setExposeAsProperty(PropertyDescriptor)}. + * + * @since 2.3.28 + */ + public void setReplaceExistingProperty(boolean overrideExistingProperty) { + this.replaceExistingProperty = overrideExistingProperty; + } + + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + */ public String getExposeMethodAs() { return exposeMethodAs; } + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + */ public void setExposeMethodAs(String exposeAsMethod) { this.exposeMethodAs = exposeAsMethod; } + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + */ public boolean getMethodShadowsProperty() { return methodShadowsProperty; } + /** + * See in the documentation of {@link MethodAppearanceFineTuner#process}. + */ public void setMethodShadowsProperty(boolean shadowEarlierProperty) { this.methodShadowsProperty = shadowEarlierProperty; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b349362c/src/main/java/freemarker/ext/beans/ClassIntrospector.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/ext/beans/ClassIntrospector.java b/src/main/java/freemarker/ext/beans/ClassIntrospector.java index ce8bbd3..3b8f6ce 100644 --- a/src/main/java/freemarker/ext/beans/ClassIntrospector.java +++ b/src/main/java/freemarker/ext/beans/ClassIntrospector.java @@ -349,7 +349,9 @@ class ClassIntrospector { } PropertyDescriptor propDesc = decision.getExposeAsProperty(); - if (propDesc != null && !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor)) { + if (propDesc != null && + (decision.getReplaceExistingProperty() + || !(introspData.get(propDesc.getName()) instanceof FastPropertyDescriptor))) { addPropertyDescriptorToClassIntrospectionData( introspData, propDesc, clazz, accessibleMethods); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b349362c/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java b/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java index 6b80893..8dd134a 100644 --- a/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java +++ b/src/main/java/freemarker/ext/beans/MethodAppearanceFineTuner.java @@ -55,7 +55,11 @@ public interface MethodAppearanceFineTuner { * {@link MethodAppearanceDecision#setExposeAsProperty(PropertyDescriptor)}. * For example, if you have <tt>int size()</tt> in a class, but you * want it to be accessed from the templates as <tt>obj.size</tt>, - * rather than as <tt>obj.size()</tt>, you can do that with this. + * rather than as <tt>obj.size()</tt>, you can do that with this + * (but remember calling + * {@link MethodAppearanceDecision#setMethodShadowsProperty(boolean) + * setMethodShadowsProperty(false)} as well, if the method name is exactly + * the same as the property name). * The default is {@code null}, which means that no fake property is * created for the method. You need not and shouldn't set this * to non-<tt>null</tt> for the getter methods of real JavaBean @@ -65,9 +69,10 @@ public interface MethodAppearanceFineTuner { * is given as the <tt>clazz</tt> parameter or it must be inherited from * that class, or else whatever errors can occur later. * {@link IndexedPropertyDescriptor}-s are supported. - * If a real JavaBean property of the same name exists, it won't be - * replaced by the fake one. Also if a fake property of the same name - * was assigned earlier, it won't be replaced. + * If a real JavaBean property of the same name exists, or a fake property + * of the same name was already assigned earlier, it won't be + * replaced by the new one by default, however this can be changed with + * {@link MethodAppearanceDecision#setReplaceExistingProperty(boolean)}. * <li>Prevent the method to hide a JavaBean property (fake or real) of * the same name by calling * {@link MethodAppearanceDecision#setMethodShadowsProperty(boolean)} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b349362c/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 769b7b1..fd6956c 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -27070,6 +27070,46 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <appendix xml:id="app_versions"> <title>Version history</title> + <section xml:id="versions_2_3_28"> + <title>2.3.28 (incubating at Apache)</title> + + <para>Release date: [FIXME]</para> + + <para><emphasis role="bold">This is a stable, final + release.</emphasis> The <quote>incubating</quote> suffix is required + by the Apache Software Foundation until the project becomes a fully + accepted (graduated) Apache project.</para> + + <section> + <title>Changes on the FTL side</title> + + <itemizedlist> + <listitem> + <para>[FIXME]</para> + </listitem> + </itemizedlist> + </section> + + <section> + <title>Changes on the Java side</title> + + <itemizedlist> + <listitem> + <para>Added new property to + <literal>BeansWrapper.MethodAppearanceDecision</literal>: + <literal>replaceExistingProperty</literal>. This is useful when + a method like <literal>size()</literal> is exposed as a JavaBean + property via + <literal>MethodAppearanceDecision.exposeAsProperty</literal>, + but there's also a <quote>real</quote> JavaBean property (like + <literal>getSize()</literal>) with identical name. By default + the real property isn't replaced, but now with + <literal>replaceExistingProperty</literal> it can be.</para> + </listitem> + </itemizedlist> + </section> + </section> + <section xml:id="versions_2_3_27"> <title>2.3.27 (incubating at Apache)</title> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/b349362c/src/test/java/freemarker/ext/beans/FineTuneMethodAppearanceTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/ext/beans/FineTuneMethodAppearanceTest.java b/src/test/java/freemarker/ext/beans/FineTuneMethodAppearanceTest.java index fa663a4..29eaa23 100644 --- a/src/test/java/freemarker/ext/beans/FineTuneMethodAppearanceTest.java +++ b/src/test/java/freemarker/ext/beans/FineTuneMethodAppearanceTest.java @@ -21,12 +21,17 @@ package freemarker.ext.beans; import static org.junit.Assert.*; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecision; +import freemarker.ext.beans.BeansWrapper.MethodAppearanceDecisionInput; +import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapper; import freemarker.template.TemplateHashModel; import freemarker.template.TemplateMethodModelEx; @@ -70,7 +75,30 @@ public class FineTuneMethodAppearanceTest { assertEquals("getV3()", ((TemplateScalarModel) thm.get("v3")).getAsString()); assertTrue(thm.get("getV3") instanceof TemplateMethodModelEx); } - + + @Test + public void existingPropertyReplacement() throws TemplateModelException { + for (Boolean replaceExistingProperty : new Boolean[] { null, false }) { + // The "real" property wins, no mater what: + assertSSubvariableValue(replaceExistingProperty, true, "from getS()"); + assertSSubvariableValue(replaceExistingProperty, false, "from getS()"); + } + + // replaceExistingProperty = true; the "real" property can be overridden: + assertSSubvariableValue(true, true, "from getS()"); + assertSSubvariableValue(true, false, "from s()"); + } + + private void assertSSubvariableValue(Boolean replaceExistingProperty, boolean preferGetS, String expectedValue) + throws TemplateModelException { + DefaultObjectWrapper ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_27); + ow.setMethodAppearanceFineTuner( + new PropertyReplacementMethodAppearanceFineTuner(replaceExistingProperty, preferGetS)); + assertEquals(expectedValue, + ((TemplateScalarModel) ((TemplateHashModel) ow.wrap(new PropertyReplacementTestBean())).get("s")) + .getAsString()); + } + static public class C { public String v1 = "v1"; @@ -98,4 +126,52 @@ public class FineTuneMethodAppearanceTest { } static class DefaultObjectWrapperOverrideExt extends DefaultObjectWrapperOverride { } + + static public class PropertyReplacementTestBean { + + public String getS() { + return "from getS()"; + } + + public String s() { + return "from s()"; + } + } + + static class PropertyReplacementMethodAppearanceFineTuner implements MethodAppearanceFineTuner { + private final Boolean replaceExistingProperty; + private final boolean preferGetS; + + PropertyReplacementMethodAppearanceFineTuner(Boolean replaceExistingProperty, boolean preferGetS) { + this.replaceExistingProperty = replaceExistingProperty; + this.preferGetS = preferGetS; + } + + public void process(MethodAppearanceDecisionInput in, MethodAppearanceDecision out) { + if (replaceExistingProperty != null) { + out.setReplaceExistingProperty(replaceExistingProperty); + } + if (preferGetS) { + if (in.getMethod().getName().equals("getS")) { + try { + out.setExposeAsProperty(new PropertyDescriptor("s", in.getMethod(), null)); + } catch (IntrospectionException e) { + throw new IllegalStateException(e); + } + } + } else { + if (in.getMethod().getName().equals("s")) { + try { + out.setExposeAsProperty(new PropertyDescriptor("s", in.getMethod(), null)); + out.setMethodShadowsProperty(false); + } catch (IntrospectionException e) { + throw new IllegalStateException(e); + } + } + } + } + + + } + }