DefaultObjectWrapper, only with its incompatible_improvements set to 2.3.26, wraps java.util.Enumeration-s into freemarker.template.DefaultEnumerationAdapter (a new class) instead of into freemarker.ext.beans.EnumerationModel (as far as useAdaptersForContainers is true, which is the default). This adapter is cleaner than EnumerationModel as it only implements the minimally required FTL type, which avoids some ambiguous situations. (Note that Java API methods aren't exposed anymore as subvariables; if you really need them, you can use ?api).
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/544e9105 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/544e9105 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/544e9105 Branch: refs/heads/2.3 Commit: 544e9105c54c0afbdcea6d2eac00cf19aa0f4daa Parents: 593d35d Author: ddekany <[email protected]> Authored: Sun Mar 12 15:42:24 2017 +0100 Committer: ddekany <[email protected]> Committed: Sun Mar 12 15:42:24 2017 +0100 ---------------------------------------------------------------------- .../template/DefaultEnumerationAdapter.java | 96 ++++++++++++++++++++ .../template/DefaultObjectWrapper.java | 16 ++++ src/manual/en_US/book.xml | 22 ++++- .../template/DefaultObjectWrapperTest.java | 40 ++++++++ 4 files changed, 172 insertions(+), 2 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/544e9105/src/main/java/freemarker/template/DefaultEnumerationAdapter.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/DefaultEnumerationAdapter.java b/src/main/java/freemarker/template/DefaultEnumerationAdapter.java new file mode 100644 index 0000000..570e0e8 --- /dev/null +++ b/src/main/java/freemarker/template/DefaultEnumerationAdapter.java @@ -0,0 +1,96 @@ +package freemarker.template; + +import java.io.Serializable; +import java.util.Enumeration; +import java.util.Iterator; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import freemarker.ext.util.WrapperTemplateModel; + +/** + * Adapts an {@link Enumeration} to the corresponding {@link TemplateModel} interface(s), most importantly to + * {@link TemplateCollectionModel}. Putting aside that it wraps an {@link Enumeration} instead of an {@link Iterator}, + * this is identical to {@link DefaultIteratorAdapter}, so see further details there. + * + * @since 2.3.26 + */ +@SuppressWarnings("serial") +public class DefaultEnumerationAdapter extends WrappingTemplateModel implements TemplateCollectionModel, + AdapterTemplateModel, WrapperTemplateModel, Serializable { + + @SuppressFBWarnings(value="SE_BAD_FIELD", justification="We hope it's Seralizable") + private final Enumeration<?> enumeration; + private boolean enumerationOwnedBySomeone; + + /** + * Factory method for creating new adapter instances. + * + * @param enumeration + * The enumeration to adapt; can't be {@code null}. + */ + public static DefaultEnumerationAdapter adapt(Enumeration<?> enumeration, ObjectWrapper wrapper) { + return new DefaultEnumerationAdapter(enumeration, wrapper); + } + + private DefaultEnumerationAdapter(Enumeration<?> enumeration, ObjectWrapper wrapper) { + super(wrapper); + this.enumeration = enumeration; + } + + @Override + public Object getWrappedObject() { + return enumeration; + } + + @Override + public Object getAdaptedObject(Class<?> hint) { + return getWrappedObject(); + } + + @Override + public TemplateModelIterator iterator() throws TemplateModelException { + return new SimpleTemplateModelIterator(); + } + + /** + * Not thread-safe. + */ + private class SimpleTemplateModelIterator implements TemplateModelIterator { + + private boolean enumerationOwnedByMe; + + @Override + public TemplateModel next() throws TemplateModelException { + if (!enumerationOwnedByMe) { + checkNotOwner(); + enumerationOwnedBySomeone = true; + enumerationOwnedByMe = true; + } + + if (!enumeration.hasMoreElements()) { + throw new TemplateModelException("The collection has no more items."); + } + + Object value = enumeration.nextElement(); + return value instanceof TemplateModel ? (TemplateModel) value : wrap(value); + } + + @Override + public boolean hasNext() throws TemplateModelException { + // Calling hasNext may looks safe, but I have met sync. problems. + if (!enumerationOwnedByMe) { + checkNotOwner(); + } + + return enumeration.hasMoreElements(); + } + + private void checkNotOwner() throws TemplateModelException { + if (enumerationOwnedBySomeone) { + throw new TemplateModelException( + "This collection value wraps a java.util.Enumeration, thus it can be listed only once."); + } + } + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/544e9105/src/main/java/freemarker/template/DefaultObjectWrapper.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/DefaultObjectWrapper.java b/src/main/java/freemarker/template/DefaultObjectWrapper.java index 1398be4..4c5a39d 100644 --- a/src/main/java/freemarker/template/DefaultObjectWrapper.java +++ b/src/main/java/freemarker/template/DefaultObjectWrapper.java @@ -22,6 +22,7 @@ package freemarker.template; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; +import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -31,6 +32,7 @@ import org.w3c.dom.Node; import freemarker.ext.beans.BeansWrapper; import freemarker.ext.beans.BeansWrapperConfiguration; +import freemarker.ext.beans.EnumerationModel; import freemarker.ext.dom.NodeModel; import freemarker.log.Logger; @@ -68,6 +70,7 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { private boolean useAdaptersForContainers; private boolean forceLegacyNonListCollections; private boolean iterableSupport; + private final boolean useAdapterForEnumerations; /** * Creates a new instance with the incompatible-improvements-version specified in @@ -97,6 +100,13 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { * won't cause the a later iteration (or further emptiness check) to fail anymore. Earlier, in * certain situations, the second operation has failed saying that the iterator "can be listed only * once". + * <li>2.3.26 (or higher): {@link Enumeration}-s are wrapped into {@link DefaultEnumerationAdapter} + * instead of into {@link EnumerationModel} (as far as + * {@link #setUseAdaptersForContainers(boolean) useAdaptersForContainers} is {@code true}, which is + * the default). This adapter is cleaner than {@link EnumerationModel} as it only implements the + * minimally required FTL type, which avoids some ambiguous situations. (Note that Java API methods + * aren't exposed anymore as subvariables; if you really need them, you can use {@code ?api}). + * </li> * </ul> * * @since 2.3.21 @@ -117,6 +127,8 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { ? (DefaultObjectWrapperConfiguration) bwCfg : new DefaultObjectWrapperConfiguration(bwCfg.getIncompatibleImprovements()) { }; useAdaptersForContainers = dowDowCfg.getUseAdaptersForContainers(); + useAdapterForEnumerations = useAdaptersForContainers + && getIncompatibleImprovements().intValue() >= _TemplateAPI.VERSION_INT_2_3_26; forceLegacyNonListCollections = dowDowCfg.getForceLegacyNonListCollections(); iterableSupport = dowDowCfg.getIterableSupport(); finalizeConstruction(writeProtected); @@ -226,9 +238,13 @@ public class DefaultObjectWrapper extends freemarker.ext.beans.BeansWrapper { ? (TemplateModel) DefaultIteratorAdapter.adapt((Iterator<?>) obj, this) : (TemplateModel) new SimpleCollection((Iterator<?>) obj, this); } + if (useAdapterForEnumerations && obj instanceof Enumeration) { + return DefaultEnumerationAdapter.adapt((Enumeration<?>) obj, this); + } if (iterableSupport && obj instanceof Iterable) { return DefaultIterableAdapter.adapt((Iterable<?>) obj, this); } + return handleUnknownType(obj); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/544e9105/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 3e0814f..beee581 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -7,9 +7,9 @@ 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 @@ -26887,6 +26887,24 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </listitem> <listitem> + <para><literal>DefaultObjectWrapper</literal>, only with its + <literal>incompatible_improvements</literal> set to 2.3.26 + (<link linkend="topic.defaultObjectWrapperIcI">see how + here...</link>), wraps + <literal>java.util.Enumeration</literal>-s into + <literal>freemarker.template.DefaultEnumerationAdapter</literal> + (a new class) instead of into + <literal>freemarker.ext.beans.EnumerationModel</literal> (as far + as <literal>useAdaptersForContainers</literal> is + <literal>true</literal>, which is the default). This adapter is + cleaner than <literal>EnumerationModel</literal> as it only + implements the minimally required FTL type, which avoids some + ambiguous situations. (Note that Java API methods aren't exposed + anymore as subvariables; if you really need them, you can use + <literal>?api</literal>).</para> + </listitem> + + <listitem> <para>Better error messages when someone tries to get an invalid <literal>@@<replaceable>...</replaceable></literal> subvariable of an XML DOM node. (Now it's not issued by the XPath http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/544e9105/src/test/java/freemarker/template/DefaultObjectWrapperTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java index 5d3ad2b..4be352a 100644 --- a/src/test/java/freemarker/template/DefaultObjectWrapperTest.java +++ b/src/test/java/freemarker/template/DefaultObjectWrapperTest.java @@ -39,6 +39,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.Vector; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -53,6 +54,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import freemarker.ext.beans.BeansWrapper; +import freemarker.ext.beans.EnumerationModel; import freemarker.ext.beans.HashAdapter; import freemarker.ext.util.WrapperTemplateModel; @@ -943,6 +945,44 @@ public class DefaultObjectWrapperTest { assertTemplateOutput(OW22IS, iterable, listingFTL, "a, b, c"); } } + + @Test + public void testNoEnumerationAdapter() throws TemplateModelException { + DefaultObjectWrapper ow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25).build(); + Vector<String> vector = new Vector<String>(); + vector.add("a"); + vector.add("b"); + + TemplateModel wrappedEnumeration = ow.wrap(vector.elements()); + assertThat(wrappedEnumeration, instanceOf(EnumerationModel.class)); + EnumerationModel enumModel = (EnumerationModel) wrappedEnumeration; + assertNotNull(enumModel.get("nextElement")); + } + + @Test + public void testEnumerationAdapter() throws TemplateModelException { + DefaultObjectWrapper ow = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_26).build(); + Vector<String> vector = new Vector<String>(); + vector.add("a"); + vector.add("b"); + + TemplateModel wrappedEnumeration = ow.wrap(vector.elements()); + assertThat(wrappedEnumeration, instanceOf(DefaultEnumerationAdapter.class)); + DefaultEnumerationAdapter enumAdapter = (DefaultEnumerationAdapter) wrappedEnumeration; + TemplateModelIterator iterator = enumAdapter.iterator(); + assertTrue(iterator.hasNext()); + assertEquals("a", ((TemplateScalarModel) iterator.next()).getAsString()); + assertTrue(iterator.hasNext()); + assertEquals("b", ((TemplateScalarModel) iterator.next()).getAsString()); + assertFalse(iterator.hasNext()); + + iterator = enumAdapter.iterator(); + try { + iterator.hasNext(); + } catch (TemplateException e) { + assertThat(e.getMessage(), containsStringIgnoringCase("only once")); + } + } @Test public void assertCanWrapDOM() throws SAXException, IOException, ParserConfigurationException,
