Added TemplateModelUtils.getKeyValuePairIterator(TemplateHashModelEx) static utility class, which can be used to get a TemplateHashModelEx2.KeyValuePairIterator even for a non-TemplateHashModelEx2 object. This simplifies Java code that iterates through key-value pairs.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/59f2e7b8 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/59f2e7b8 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/59f2e7b8 Branch: refs/heads/2.3 Commit: 59f2e7b8c082b6405850fc2163b78fac3cbdd4dc Parents: c533df5 Author: ddekany <ddek...@apache.org> Authored: Wed Feb 28 11:31:37 2018 +0100 Committer: ddekany <ddek...@apache.org> Committed: Wed Feb 28 11:31:37 2018 +0100 ---------------------------------------------------------------------- .../java/freemarker/core/IteratorBlock.java | 13 +- src/main/java/freemarker/core/_MessageUtil.java | 8 +- .../template/utility/TemplateModelUtils.java | 90 +++++++++++ src/manual/en_US/book.xml | 12 +- .../template/utility/TemplateModelUtilTest.java | 155 +++++++++++++++++++ 5 files changed, 262 insertions(+), 16 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59f2e7b8/src/main/java/freemarker/core/IteratorBlock.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/IteratorBlock.java b/src/main/java/freemarker/core/IteratorBlock.java index 83c0b22..bbb6773 100644 --- a/src/main/java/freemarker/core/IteratorBlock.java +++ b/src/main/java/freemarker/core/IteratorBlock.java @@ -392,17 +392,8 @@ final class IteratorBlock extends TemplateElement { listLoop: do { loopVar = keysIter.next(); if (!(loopVar instanceof TemplateScalarModel)) { - throw new NonStringException(env, - new _ErrorDescriptionBuilder( - "When listing key-value pairs of traditional hash " - + "implementations, all keys must be strings, but one of them " - + "was ", - new _DelayedAOrAn(new _DelayedFTLTypeDescription(loopVar)), "." - ).tip("The listed value's TemplateModel class was ", - new _DelayedShortClassName(listedValue.getClass()), - ", which doesn't implement ", - new _DelayedShortClassName(TemplateHashModelEx2.class), - ", which leads to this restriction.")); + throw _MessageUtil.newKeyValuePairListingNonStringKeyExceptionMessage( + loopVar, (TemplateHashModelEx) listedValue); } loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString()); hasNext = keysIter.hasNext(); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59f2e7b8/src/main/java/freemarker/core/_MessageUtil.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java index db097db..42cfac7 100644 --- a/src/main/java/freemarker/core/_MessageUtil.java +++ b/src/main/java/freemarker/core/_MessageUtil.java @@ -322,10 +322,10 @@ public class _MessageUtil { ? new _TemplateModelException(e, (Environment) null, desc) : new _MiscTemplateException(e, (Environment) null, desc); } - - public static _ErrorDescriptionBuilder traditionalHashExKeyMustBeStringExceptionMessage( + + public static TemplateModelException newKeyValuePairListingNonStringKeyExceptionMessage( TemplateModel key, TemplateHashModelEx listedHashEx) { - return new _ErrorDescriptionBuilder( + return new _TemplateModelException(new _ErrorDescriptionBuilder( "When listing key-value pairs of traditional hash " + "implementations, all keys must be strings, but one of them " + "was ", @@ -334,7 +334,7 @@ public class _MessageUtil { new _DelayedShortClassName(listedHashEx.getClass()), ", which doesn't implement ", new _DelayedShortClassName(TemplateHashModelEx2.class), - ", which leads to this restriction."); + ", which leads to this restriction.")); } /** http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59f2e7b8/src/main/java/freemarker/template/utility/TemplateModelUtils.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/utility/TemplateModelUtils.java b/src/main/java/freemarker/template/utility/TemplateModelUtils.java new file mode 100644 index 0000000..d33b6da --- /dev/null +++ b/src/main/java/freemarker/template/utility/TemplateModelUtils.java @@ -0,0 +1,90 @@ +/* + * 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.template.utility; + +import freemarker.core._MessageUtil; +import freemarker.template.TemplateHashModelEx; +import freemarker.template.TemplateHashModelEx2; +import freemarker.template.TemplateHashModelEx2.KeyValuePair; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateModelIterator; +import freemarker.template.TemplateScalarModel; + +/** + * Static utility method related to {@link TemplateModel}-s that didn't fit elsewhere. + * + * @since 2.3.28 + */ +public final class TemplateModelUtils { + + // Private to prevent instantiation + private TemplateModelUtils() { + // no op. + } + + /** + * {@link TemplateHashModelExKeyValuePairIterator} that even works for a non-{@link TemplateHashModelEx2} + * {@link TemplateHashModelEx}. This is used to simplify code that needs to iterate through the key-value pairs of + * {@link TemplateHashModelEx}-s, as with this you don't have to handle non-{@link TemplateHashModelEx2}-s + * separately. For non-{@link TemplateHashModelEx2} values the iteration will throw {@link TemplateModelException} + * if it reaches a key that's not a string ({@link TemplateScalarModel}). + */ + public static final TemplateHashModelEx2.KeyValuePairIterator getKeyValuePairIterator(TemplateHashModelEx hash) + throws TemplateModelException { + return hash instanceof TemplateHashModelEx2 ? ((TemplateHashModelEx2) hash).keyValuePairIterator() + : new TemplateHashModelExKeyValuePairIterator(hash); + } + + private static class TemplateHashModelExKeyValuePairIterator implements TemplateHashModelEx2.KeyValuePairIterator { + + private final TemplateHashModelEx hash; + private final TemplateModelIterator keyIter; + + private TemplateHashModelExKeyValuePairIterator(TemplateHashModelEx hash) throws TemplateModelException { + this.hash = hash; + keyIter = hash.keys().iterator(); + } + + public boolean hasNext() throws TemplateModelException { + return keyIter.hasNext(); + } + + public KeyValuePair next() throws TemplateModelException { + final TemplateModel key = keyIter.next(); + if (!(key instanceof TemplateScalarModel)) { + throw _MessageUtil.newKeyValuePairListingNonStringKeyExceptionMessage(key, hash); + } + + return new KeyValuePair() { + + public TemplateModel getKey() throws TemplateModelException { + return key; + } + + public TemplateModel getValue() throws TemplateModelException { + return hash.get(((TemplateScalarModel) key).getAsString()); + } + + }; + } + + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59f2e7b8/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index b208ecd..56851f3 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -30,7 +30,7 @@ <titleabbrev>Manual</titleabbrev> - <productname>Freemarker 2.3.27</productname> + <productname>Freemarker 2.3.28</productname> </info> <preface role="index.html" xml:id="preface"> @@ -27157,6 +27157,16 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </listitem> </itemizedlist> </listitem> + + <listitem> + <para>Added + <literal>TemplateModelUtils.getKeyValuePairIterator(TemplateHashModelEx)</literal> + static utility class, which can be used to get a + <literal>TemplateHashModelEx2.KeyValuePairIterator</literal> + even for a non-<literal>TemplateHashModelEx2</literal> object. + This simplifies Java code that iterates through key-value + pairs.</para> + </listitem> </itemizedlist> </section> </section> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/59f2e7b8/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java b/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java new file mode 100644 index 0000000..6179e17 --- /dev/null +++ b/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java @@ -0,0 +1,155 @@ +/* + * 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.template.utility; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; + +import freemarker.template.Configuration; +import freemarker.template.DefaultMapAdapter; +import freemarker.template.DefaultNonListCollectionAdapter; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.TemplateCollectionModel; +import freemarker.template.TemplateHashModelEx; +import freemarker.template.TemplateHashModelEx2.KeyValuePair; +import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.template.TemplateNumberModel; +import freemarker.template.TemplateScalarModel; +import freemarker.test.TemplateTest; + +public class TemplateModelUtilTest extends TemplateTest { + + @Test + public void testGetKeyValuePairIterator() throws Exception { + Map<Object, Object> map = new LinkedHashMap<Object, Object>(); + TemplateHashModelEx thme = new TemplateHashModelExOnly(map); + + assertetKeyValuePairIteratorResult("", thme); + + map.put("k1", 11); + assertetKeyValuePairIteratorResult("str(k1): num(11)", thme); + + map.put("k2", "v2"); + assertetKeyValuePairIteratorResult("str(k1): num(11), str(k2): str(v2)", thme); + + map.put("k2", null); + assertetKeyValuePairIteratorResult("str(k1): num(11), str(k2): null", thme); + + map.put(3, 33); + try { + assertetKeyValuePairIteratorResult("fails anyway...", thme); + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), + allOf(containsString("keys must be"), containsString("string"), containsString("number"))); + } + map.remove(3); + + map.put(null, 44); + try { + assertetKeyValuePairIteratorResult("fails anyway...", thme); + fail(); + } catch (TemplateModelException e) { + assertThat(e.getMessage(), + allOf(containsString("keys must be"), containsString("string"), containsString("Null"))); + } + } + + @Test + public void testGetKeyValuePairIteratorWithEx2() throws Exception { + Map<Object, Object> map = new LinkedHashMap<Object, Object>(); + TemplateHashModelEx thme = DefaultMapAdapter.adapt( + map, (ObjectWrapperWithAPISupport) getConfiguration().getObjectWrapper()); + + assertetKeyValuePairIteratorResult("", thme); + + map.put("k1", 11); + map.put("k2", "v2"); + map.put("k2", null); + map.put(3, 33); + map.put(null, 44); + assertetKeyValuePairIteratorResult("str(k1): num(11), str(k2): null, num(3): num(33), null: num(44)", thme); + } + + private void assertetKeyValuePairIteratorResult(String expected, TemplateHashModelEx thme) + throws TemplateModelException { + StringBuilder sb = new StringBuilder(); + KeyValuePairIterator kvpi = TemplateModelUtils.getKeyValuePairIterator(thme); + while (kvpi.hasNext()) { + KeyValuePair kvp = kvpi.next(); + if (sb.length() != 0) { + sb.append(", "); + } + sb.append(toAssertionString(kvp.getKey())).append(": ").append(toAssertionString(kvp.getValue())); + } + } + + private String toAssertionString(TemplateModel model) throws TemplateModelException { + if (model instanceof TemplateNumberModel) { + return "num(" + ((TemplateNumberModel) model).getAsNumber() + ")"; + } else if (model instanceof TemplateScalarModel) { + return "str(" + ((TemplateScalarModel) model).getAsString() + ")"; + } else if (model == null) { + return "null"; + } + + throw new IllegalArgumentException("Type unsupported by test: " + model.getClass().getName()); + } + + private static class TemplateHashModelExOnly implements TemplateHashModelEx { + + private final Map<?, ?> map; + private final ObjectWrapperWithAPISupport objectWrapper; + + public TemplateHashModelExOnly(Map<?, ?> map) { + this.map = map; + objectWrapper = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build(); + } + + public TemplateModel get(String key) throws TemplateModelException { + return objectWrapper.wrap(map.get(key)); + } + + public boolean isEmpty() throws TemplateModelException { + return map.isEmpty(); + } + + public int size() throws TemplateModelException { + return 2; + } + + public TemplateCollectionModel keys() throws TemplateModelException { + return DefaultNonListCollectionAdapter.adapt(map.keySet(), objectWrapper); + } + + public TemplateCollectionModel values() throws TemplateModelException { + return DefaultNonListCollectionAdapter.adapt(map.values(), objectWrapper); + } + + } + +}