Added the continue directive, which can be used inside a list to skip to the next iteration (similarly as in Java).
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/da570caa Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/da570caa Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/da570caa Branch: refs/heads/2.3 Commit: da570caa85c9b17ece822fe72fcce22502f57b54 Parents: 3dfa8ce Author: ddekany <[email protected]> Authored: Sun Sep 17 12:46:02 2017 +0200 Committer: ddekany <[email protected]> Committed: Sun Sep 17 12:46:02 2017 +0200 ---------------------------------------------------------------------- .../java/freemarker/core/BreakInstruction.java | 8 +- .../core/BreakOrContinueException.java | 12 ++ .../freemarker/core/ContinueInstruction.java | 64 ++++++++++ .../java/freemarker/core/IteratorBlock.java | 102 ++++++++-------- src/main/java/freemarker/core/SwitchBlock.java | 2 +- src/main/java/freemarker/core/_CoreAPI.java | 1 + src/main/javacc/FTL.jj | 41 +++++++ src/manual/en_US/book.xml | 116 ++++++++++++++++--- .../core/BreakAndContinuePlacementTest.java | 75 ++++++++++++ .../freemarker/core/BreakPlacementTest.java | 68 ----------- .../freemarker/core/ListBreakContinueTest.java | 93 +++++++++++++++ 11 files changed, 443 insertions(+), 139 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/BreakInstruction.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BreakInstruction.java b/src/main/java/freemarker/core/BreakInstruction.java index b41f05b..e18f6d0 100644 --- a/src/main/java/freemarker/core/BreakInstruction.java +++ b/src/main/java/freemarker/core/BreakInstruction.java @@ -26,7 +26,7 @@ final class BreakInstruction extends TemplateElement { @Override TemplateElement[] accept(Environment env) { - throw Break.INSTANCE; + throw BreakOrContinueException.BREAK_INSTANCE; } @Override @@ -54,12 +54,6 @@ final class BreakInstruction extends TemplateElement { throw new IndexOutOfBoundsException(); } - static class Break extends RuntimeException { - static final Break INSTANCE = new Break(); - private Break() { - } - } - @Override boolean isNestedBlockRepeater() { return false; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/BreakOrContinueException.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BreakOrContinueException.java b/src/main/java/freemarker/core/BreakOrContinueException.java new file mode 100644 index 0000000..86bfbd8 --- /dev/null +++ b/src/main/java/freemarker/core/BreakOrContinueException.java @@ -0,0 +1,12 @@ +package freemarker.core; + +/** + * Used for implementing #break and #continue. + */ +class BreakOrContinueException extends RuntimeException { + + static final BreakOrContinueException BREAK_INSTANCE = new BreakOrContinueException(); + static final BreakOrContinueException CONTINUE_INSTANCE = new BreakOrContinueException(); + + private BreakOrContinueException() { } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/ContinueInstruction.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/ContinueInstruction.java b/src/main/java/freemarker/core/ContinueInstruction.java new file mode 100644 index 0000000..21ff84f --- /dev/null +++ b/src/main/java/freemarker/core/ContinueInstruction.java @@ -0,0 +1,64 @@ +/* + * 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.core; + +/** + * Represents a <break> instruction to break out of a loop. + */ +final class ContinueInstruction extends TemplateElement { + + @Override + TemplateElement[] accept(Environment env) { + throw BreakOrContinueException.CONTINUE_INSTANCE; + } + + @Override + protected String dump(boolean canonical) { + return canonical ? "<" + getNodeTypeSymbol() + "/>" : getNodeTypeSymbol(); + } + + @Override + String getNodeTypeSymbol() { + return "#continue"; + } + + @Override + int getParameterCount() { + return 0; + } + + @Override + Object getParameterValue(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + ParameterRole getParameterRole(int idx) { + throw new IndexOutOfBoundsException(); + } + + @Override + boolean isNestedBlockRepeater() { + return false; + } + +} + + http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/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 0d9fcec..83c0b22 100644 --- a/src/main/java/freemarker/core/IteratorBlock.java +++ b/src/main/java/freemarker/core/IteratorBlock.java @@ -284,16 +284,18 @@ final class IteratorBlock extends TemplateElement { listNotEmpty = iterModel.hasNext(); if (listNotEmpty) { if (loopVarName != null) { - try { - do { + listLoop: do { loopVar = iterModel.next(); hasNext = iterModel.hasNext(); - env.visit(childBuffer); + try { + env.visit(childBuffer); + } catch (BreakOrContinueException br) { + if (br == BreakOrContinueException.BREAK_INSTANCE) { + break listLoop; + } + } index++; } while (hasNext); - } catch (BreakInstruction.Break br) { - // Silently exit loop - } openedIterator = null; } else { // We must reuse this later, because TemplateCollectionModel-s that wrap an Iterator only @@ -308,15 +310,17 @@ final class IteratorBlock extends TemplateElement { listNotEmpty = size != 0; if (listNotEmpty) { if (loopVarName != null) { - try { - for (index = 0; index < size; index++) { + listLoop: for (index = 0; index < size; index++) { loopVar = seqModel.get(index); hasNext = (size > index + 1); - env.visit(childBuffer); + try { + env.visit(childBuffer); + } catch (BreakOrContinueException br) { + if (br == BreakOrContinueException.BREAK_INSTANCE) { + break listLoop; + } + } } - } catch (BreakInstruction.Break br) { - // Silently exit loop - } } else { env.visit(childBuffer); } @@ -329,7 +333,7 @@ final class IteratorBlock extends TemplateElement { } try { env.visit(childBuffer); - } catch (BreakInstruction.Break br) { + } catch (BreakOrContinueException br) { // Silently exit "loop" } } else if (listedValue instanceof TemplateHashModelEx @@ -359,18 +363,20 @@ final class IteratorBlock extends TemplateElement { hashNotEmpty = kvpIter.hasNext(); if (hashNotEmpty) { if (loopVarName != null) { - try { - do { - KeyValuePair kvp = kvpIter.next(); - loopVar = kvp.getKey(); - loopVar2 = kvp.getValue(); - hasNext = kvpIter.hasNext(); + listLoop: do { + KeyValuePair kvp = kvpIter.next(); + loopVar = kvp.getKey(); + loopVar2 = kvp.getValue(); + hasNext = kvpIter.hasNext(); + try { env.visit(childBuffer); - index++; - } while (hasNext); - } catch (BreakInstruction.Break br) { - // Silently exit loop - } + } catch (BreakOrContinueException br) { + if (br == BreakOrContinueException.BREAK_INSTANCE) { + break listLoop; + } + } + index++; + } while (hasNext); openedIterator = null; } else { // We will reuse this at the #iterms @@ -383,30 +389,32 @@ final class IteratorBlock extends TemplateElement { hashNotEmpty = keysIter.hasNext(); if (hashNotEmpty) { if (loopVarName != null) { - try { - 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.")); - } - loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString()); - hasNext = keysIter.hasNext(); + 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.")); + } + loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString()); + hasNext = keysIter.hasNext(); + try { env.visit(childBuffer); - index++; - } while (hasNext); - } catch (BreakInstruction.Break br) { - // Silently exit loop - } + } catch (BreakOrContinueException br) { + if (br == BreakOrContinueException.BREAK_INSTANCE) { + break listLoop; + } + } + index++; + } while (hasNext); } else { env.visit(childBuffer); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/SwitchBlock.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/SwitchBlock.java b/src/main/java/freemarker/core/SwitchBlock.java index 3f8a320..19c6a3e 100644 --- a/src/main/java/freemarker/core/SwitchBlock.java +++ b/src/main/java/freemarker/core/SwitchBlock.java @@ -86,7 +86,7 @@ final class SwitchBlock extends TemplateElement { if (!processedCase && defaultCase != null) { env.visit(defaultCase); } - } catch (BreakInstruction.Break br) {} + } catch (BreakOrContinueException br) {} return null; } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/java/freemarker/core/_CoreAPI.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java index 1f10f81..3491568 100644 --- a/src/main/java/freemarker/core/_CoreAPI.java +++ b/src/main/java/freemarker/core/_CoreAPI.java @@ -78,6 +78,7 @@ public class _CoreAPI { addName(allNames, lcNames, ccNames, "case"); addName(allNames, lcNames, ccNames, "comment"); addName(allNames, lcNames, ccNames, "compress"); + addName(allNames, lcNames, ccNames, "continue"); addName(allNames, lcNames, ccNames, "default"); addName(allNames, lcNames, ccNames, "else"); addName(allNames, lcNames, ccNames, "elseif", "elseIf"); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/main/javacc/FTL.jj ---------------------------------------------------------------------- diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj index b7bdd48..ddbf78f 100644 --- a/src/main/javacc/FTL.jj +++ b/src/main/javacc/FTL.jj @@ -85,6 +85,11 @@ public class FMParser { */ private int breakableDirectiveNesting; + /** + * Keeps track of the nesting depth of directives that support #continue. + */ + private int continuableDirectiveNesting; + private boolean inMacro, inFunction; private LinkedList escapes = new LinkedList(); private int mixedContentNesting; // for stripText @@ -1019,6 +1024,8 @@ TOKEN: | <BREAK : <START_TAG> "break" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); } | + <CONTINUE : <START_TAG> "continue" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); } + | <SIMPLE_RETURN : <START_TAG> "return" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); } | <HALT : <START_TAG> "stop" <CLOSE_TAG2>> { strictSyntaxCheck(matchedToken, DEFAULT); } @@ -2580,6 +2587,7 @@ TemplateElement List() : if (loopVar != null) { iterCtx.loopVarName = loopVar.image; breakableDirectiveNesting++; + continuableDirectiveNesting++; if (loopVar2 != null) { iterCtx.loopVar2Name = loopVar2.image; iterCtx.hashListing = true; @@ -2596,6 +2604,7 @@ TemplateElement List() : { if (loopVar != null) { breakableDirectiveNesting--; + continuableDirectiveNesting--; } else if (iterCtx.kind != ITERATOR_BLOCK_KIND_ITEMS) { throw new ParseException( "#list must have either \"as loopVar\" parameter or nested #items that belongs to it.", @@ -2660,6 +2669,7 @@ IteratorBlock ForEach() : iterCtx.loopVarName = loopVar.image; iterCtx.kind = ITERATOR_BLOCK_KIND_FOREACH; breakableDirectiveNesting++; + continuableDirectiveNesting++; } children = MixedContentElements() @@ -2667,6 +2677,7 @@ IteratorBlock ForEach() : end = <END_FOREACH> { breakableDirectiveNesting--; + continuableDirectiveNesting--; popIteratorBlockContext(); IteratorBlock result = new IteratorBlock(exp, loopVar.image, null, children, false, true); @@ -2718,6 +2729,7 @@ Items Items() : } breakableDirectiveNesting++; + continuableDirectiveNesting++; } children = MixedContentElements() @@ -2725,6 +2737,7 @@ Items Items() : end = <END_ITEMS> { breakableDirectiveNesting--; + continuableDirectiveNesting--; iterCtx.loopVarName = null; iterCtx.loopVar2Name = null; @@ -2851,6 +2864,27 @@ BreakInstruction Break() : } /** + * Production used to skip an iteration in a loop. + */ +ContinueInstruction Continue() : +{ + Token start; +} +{ + start = <CONTINUE> + { + if (continuableDirectiveNesting < 1) { + throw new ParseException(start.image + " must be nested inside a directive that supports it: " + + " #list with \"as\", #items (or the deprecated " + forEachDirectiveSymbol() + ")", + template, start); + } + ContinueInstruction result = new ContinueInstruction(); + result.setLocation(template, start, start); + return result; + } +} + +/** * Production used to jump out of a macro. * The stop instruction terminates the rendering of the template. */ @@ -3221,6 +3255,7 @@ Macro Macro() : Expression defValue = null; List lastIteratorBlockContexts; int lastBreakableDirectiveNesting; + int lastContiunableDirectiveNesting; TemplateElements children; boolean isFunction = false, hasDefaults = false; boolean isCatchAll = false; @@ -3292,9 +3327,12 @@ Macro Macro() : iteratorBlockContexts = null; if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_23) { lastBreakableDirectiveNesting = breakableDirectiveNesting; + lastContiunableDirectiveNesting = continuableDirectiveNesting; breakableDirectiveNesting = 0; + continuableDirectiveNesting = 0; } else { lastBreakableDirectiveNesting = 0; // Just to prevent uninitialized local variable error later + lastContiunableDirectiveNesting = 0; } } children = MixedContentElements() @@ -3313,6 +3351,7 @@ Macro Macro() : iteratorBlockContexts = lastIteratorBlockContexts; if (incompatibleImprovements >= _TemplateAPI.VERSION_INT_2_3_23) { breakableDirectiveNesting = lastBreakableDirectiveNesting; + continuableDirectiveNesting = lastContiunableDirectiveNesting; } inMacro = inFunction = false; @@ -3937,6 +3976,8 @@ TemplateElement FreemarkerDirective() : | tp = Break() | + tp = Continue() + | tp = Return() | tp = Stop() http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index f1dc57a..21c3479 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -20428,7 +20428,7 @@ All rights reserved.</emphasis></programlisting> </section> <section xml:id="ref_directive_list"> - <title>list, else, items, sep, break</title> + <title>list, else, items, sep, break, continue</title> <anchor xml:id="ref.directive.list"/> @@ -20522,7 +20522,7 @@ All rights reserved.</emphasis></programlisting> <section> <title>Description</title> - <section> + <section xml:id="ref_list_simple"> <title>Simplest form</title> <para>Assuming <literal>users</literal> contains the @@ -20569,7 +20569,7 @@ All rights reserved.</emphasis></programlisting> <literal>Map</literal> objects can be listed.</para> </section> - <section> + <section xml:id="ref_list_else"> <title>else directive</title> <anchor xml:id="ref.directive.list.else"/> @@ -20609,7 +20609,7 @@ All rights reserved.</emphasis></programlisting> included template.</para> </section> - <section> + <section xml:id="ref_list_items"> <title>items directive</title> <anchor xml:id="ref.directive.items"/> @@ -20712,7 +20712,7 @@ All rights reserved.</emphasis></programlisting> </itemizedlist> </section> - <section> + <section xml:id="ref_list_sep"> <title>sep directive</title> <anchor xml:id="ref.directive.sep"/> @@ -20765,7 +20765,7 @@ All rights reserved.</emphasis></programlisting> will be called from).</para> </section> - <section> + <section xml:id="ref_list_break"> <title>break directive</title> <anchor xml:id="ref.directive.list.break"/> @@ -20792,24 +20792,100 @@ All rights reserved.</emphasis></programlisting> anywhere inside <literal>list</literal> as far as it has <literal>as <replaceable>item</replaceable></literal> parameter, otherwise it can be placed anywhere inside the - <literal>items</literal> directive. If the - <literal>break</literal> is inside <literal>items</literal>, it - will only exit from <literal>items</literal>, not from - <literal>list</literal>. In general, <literal>break</literal> will - only exit from the directive whose body is called for each item, - and can only be placed inside such directive. So for example can't - use <literal>break</literal> inside <literal>list</literal>'s + <literal>items</literal> directive. However, it's strongly + recommended to place it either before or after all the other + things that you do inside the iteration. Otherwise it's easy to + end up with unclosed elements in the output, or otherwise make the + template harder to understand. Especially, avoid breaking out from + the nested content of custom directives (like <literal><#list + ...>...<@foo>...<#break>...</@foo>...</#list></literal>), + as the author of the directive may not expect that the closing tag + (<literal></@foo></literal>) is never executed.</para> + + <para>If the <literal>break</literal> is inside + <literal>items</literal>, it will only exit from + <literal>items</literal>, not from <literal>list</literal>. In + general, <literal>break</literal> will only exit from the + directive whose body is called for each item, and can only be + placed inside such directive. So for example can't use + <literal>break</literal> inside <literal>list</literal>'s <literal>else</literal> section, unless there's the <literal>list</literal> is nested into another <literal>break</literal>-able directive.</para> + <para>Using <literal>break</literal> together with + <literal>sep</literal> is generally a bad idea, as + <literal>sep</literal> can't know if you will skip the rest of + items with <literal>break</literal>, and then you end up with a + separator after the item printed last.</para> + <para>Just like <literal>else</literal> and <literal>items</literal>, <literal>break</literal> must be literally inside body of the directive to break out from, and can't be moved out into a macro or included template.</para> </section> - <section> + <section xml:id="ref_list_continue"> + <title>continue directive</title> + + <anchor xml:id="ref.directive.list.continue"/> + + <indexterm> + <primary>continue directive</primary> + </indexterm> + + <note> + <para>The <literal>continue</literal> directive exists since + FreeMarker 2.3.27</para> + </note> + + <para>You can skip the rest of the iteration body (the section + until the <literal></#list></literal> or + <literal></#items></literal> tag) with the + <literal>continue</literal> directive, then FreeMarker will + continue with the next item. For example:</para> + + <programlisting role="template"><#list 1..5 as x> + <#if x == 3> + <#continue> + </#if> + ${x} +</#list></programlisting> + + <programlisting role="output"> 1 + 2 + 4 + 5</programlisting> + + <para>The <literal>continue</literal> directives can be placed + anywhere inside <literal>list</literal> as far as it has + <literal>as <replaceable>item</replaceable></literal> parameter, + otherwise it can be placed anywhere inside the + <literal>items</literal> directive. However, it's strongly + recommended to place it before all the other things you do inside + the iteration. Otherwise it's easy to end up with unclosed + elements in the output, or otherwise make the template harder to + understand. Especially, avoid breaking out from the nested content + of custom directives (like <literal><#list + ...>...<@foo>...<#continue>...</@foo>...</#list></literal>), + as the author of the directive may not expect that the closing tag + (<literal></@foo></literal>) is never executed.</para> + + <para>When you call <literal>continue</literal>, the + <literal>sep</literal> directive will not be executed for that + iteration. Using <literal>continue</literal> together with + <literal>sep</literal> is generally a bad idea, as + <literal>sep</literal> can't know if you will skip the rest of the + items, and then you end up with a separator after the item printed + last.</para> + + <para>Just like <literal>break</literal>, + <literal>continue</literal> must be literally inside body of the + directive whose iteration need to be <quote>continued</quote>, and + can't be moved out into a macro or included template.</para> + </section> + + <section xml:id="ref_list_accessing_state"> <title>Accessing iteration state</title> <indexterm> @@ -20876,7 +20952,7 @@ All rights reserved.</emphasis></programlisting> 1}</literal>.</para> </section> - <section> + <section xml:id="ref_list_nesting"> <title>Nesting loops into each other</title> <para>Naturally, <literal>list</literal> or @@ -20919,7 +20995,7 @@ All rights reserved.</emphasis></programlisting> Outer again: 2</programlisting> </section> - <section> + <section xml:id="ref_list_java_notes"> <title>Notes for Java programmers</title> <para><phrase role="forProgrammers">If classic compatible mode @@ -26913,6 +26989,14 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> <itemizedlist> <listitem> + <para>Added the <link + linkend="ref.directive.list.continue"><literal>continue</literal> + directive</link>, which can be used inside a + <literal>list</literal> to skip to the next iteration (similarly + as in Java).</para> + </listitem> + + <listitem> <para>Added alternative syntaxes for the <literal>&&</literal> (logical <quote>and</quote>) operator: <literal>\and</literal> and http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java b/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java new file mode 100644 index 0000000..e4ef01c --- /dev/null +++ b/src/test/java/freemarker/core/BreakAndContinuePlacementTest.java @@ -0,0 +1,75 @@ +/* + * 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.core; + +import java.io.IOException; + +import org.junit.Test; + +import freemarker.template.Configuration; +import freemarker.template.TemplateException; +import freemarker.test.TemplateTest; + +public class BreakAndContinuePlacementTest extends TemplateTest { + + private static final String BREAK_NESTING_ERROR_MESSAGE_PART = "<#break> must be nested"; + private static final String CONTINUE_NESTING_ERROR_MESSAGE_PART = "<#continue> must be nested"; + + @Test + public void testValidPlacements() throws IOException, TemplateException { + assertOutput("<#assign x = 1><#switch x><#case 1>one<#break><#case 2>two</#switch>", "one"); + assertOutput("<#list 1..2 as x>${x}<#break></#list>", "1"); + assertOutput("<#list 1..2 as x>${x}<#continue></#list>", "12"); + assertOutput("<#list 1..2>[<#items as x>${x}<#break></#items>]</#list>", "[1]"); + assertOutput("<#list 1..2 as x>${x}<#list 1..3>B<#break>E<#items as y></#items></#list>E</#list>.", "1B."); + assertOutput("<#list 1..2 as x>${x}<#list 3..4 as x>${x}<#break></#list>;</#list>", "13;23;"); + assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>[<#list xs as x>${x}<#else><#break></#list>]</#list>.", + "[12][34][."); + assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>" + + "<#list xs>[<#items as x>${x}</#items>]<#else><#break></#list>" + + "</#list>.", + "[12][34]."); + assertOutput("<#forEach x in 1..2>${x}<#break></#forEach>", "1"); + assertOutput("<#forEach x in 1..2>${x}<#continue></#forEach>", "12"); + assertOutput("<#switch 1><#case 1>1<#break></#switch>", "1"); + } + + @Test + public void testInvalidPlacements() throws IOException, TemplateException { + assertErrorContains("<#break>", BREAK_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#continue>", CONTINUE_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#switch 1><#case 1>1<#continue></#switch>", CONTINUE_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#list 1..2 as x>${x}</#list><#break>", BREAK_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#if false><#break></#if>", BREAK_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#list xs><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART); + assertErrorContains("<#list 1..2 as x>${x}<#else><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART); + } + + @Test + public void testInvalidPlacementMacroLoophole() throws IOException, TemplateException { + final String ftl = "<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>"; + getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_22); + assertOutput(ftl, "12"); + getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_23); + assertErrorContains(ftl, BREAK_NESTING_ERROR_MESSAGE_PART); + assertErrorContains(ftl.replaceAll("#break", "#continue"), CONTINUE_NESTING_ERROR_MESSAGE_PART); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/test/java/freemarker/core/BreakPlacementTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/BreakPlacementTest.java b/src/test/java/freemarker/core/BreakPlacementTest.java deleted file mode 100644 index 8844956..0000000 --- a/src/test/java/freemarker/core/BreakPlacementTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.core; - -import java.io.IOException; - -import org.junit.Test; - -import freemarker.template.Configuration; -import freemarker.template.TemplateException; -import freemarker.test.TemplateTest; - -public class BreakPlacementTest extends TemplateTest { - - private static final String BREAK_NESTING_ERROR_MESSAGE_PART = "<#break> must be nested"; - - @Test - public void testValidPlacements() throws IOException, TemplateException { - assertOutput("<#assign x = 1><#switch x><#case 1>one<#break><#case 2>two</#switch>", "one"); - assertOutput("<#list 1..2 as x>${x}<#break></#list>", "1"); - assertOutput("<#list 1..2>[<#items as x>${x}<#break></#items>]</#list>", "[1]"); - assertOutput("<#list 1..2 as x>${x}<#list 1..3>B<#break>E<#items as y></#items></#list>E</#list>.", "1B."); - assertOutput("<#list 1..2 as x>${x}<#list 3..4 as x>${x}<#break></#list>;</#list>", "13;23;"); - assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>[<#list xs as x>${x}<#else><#break></#list>]</#list>.", - "[12][34][."); - assertOutput("<#list [1..2, 3..4, [], 5..6] as xs>" - + "<#list xs>[<#items as x>${x}</#items>]<#else><#break></#list>" - + "</#list>.", - "[12][34]."); - assertOutput("<#forEach x in 1..2>${x}<#break></#forEach>", "1"); - } - - @Test - public void testInvalidPlacements() throws IOException, TemplateException { - assertErrorContains("<#break>", BREAK_NESTING_ERROR_MESSAGE_PART); - assertErrorContains("<#list 1..2 as x>${x}</#list><#break>", BREAK_NESTING_ERROR_MESSAGE_PART); - assertErrorContains("<#if false><#break></#if>", BREAK_NESTING_ERROR_MESSAGE_PART); - assertErrorContains("<#list xs><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART); - assertErrorContains("<#list 1..2 as x>${x}<#else><#break></#list>", BREAK_NESTING_ERROR_MESSAGE_PART); - } - - @Test - public void testInvalidPlacementMacroLoophole() throws IOException, TemplateException { - final String ftl = "<#list 1..2 as x>${x}<#macro m><#break></#macro></#list>"; - getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_22); - assertOutput(ftl, "12"); - getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_23); - assertErrorContains(ftl, BREAK_NESTING_ERROR_MESSAGE_PART); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/da570caa/src/test/java/freemarker/core/ListBreakContinueTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/ListBreakContinueTest.java b/src/test/java/freemarker/core/ListBreakContinueTest.java new file mode 100644 index 0000000..8ee4af8 --- /dev/null +++ b/src/test/java/freemarker/core/ListBreakContinueTest.java @@ -0,0 +1,93 @@ +package freemarker.core; + +import java.io.IOException; + +import org.junit.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.TemplateCollectionModel; +import freemarker.template.TemplateException; +import freemarker.template.TemplateHashModelEx; +import freemarker.template.TemplateModel; +import freemarker.template.TemplateModelException; +import freemarker.test.TemplateTest; + +public class ListBreakContinueTest extends TemplateTest { + + @Test + public void testNonHash() throws IOException, TemplateException { + testNonHash(ImmutableList.of(1, 2, 3, 4, 5)); // Listing a TemplateSequenceModel + testNonHash(ImmutableSet.of(1, 2, 3, 4, 5)); // Listing a TemplateCollectionModel + } + + private void testNonHash(Object listed) throws IOException, TemplateException { + addToDataModel("listed", listed); + assertOutput( + "<#list listed as i>B(${i}) <#if i == 3>Break!<#break></#if>A(${i})<#sep>, </#list>", + "B(1) A(1), B(2) A(2), B(3) Break!"); + assertOutput( + "<#list listed as i>B(${i}) <#if i == 3>Continue! <#continue></#if>A(${i})<#sep>, </#list>", + "B(1) A(1), B(2) A(2), B(3) Continue! B(4) A(4), B(5) A(5)"); + } + + @Test + public void testHash() throws IOException, TemplateException { + testHash(ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5)); // Listing a TemplateHashModelEx2 + testHash(new NonEx2Hash((TemplateHashModelEx) getConfiguration().getObjectWrapper().wrap( + ImmutableMap.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5)))); // Listing a TemplateHashModelEx (non-Ex2) + } + + private void testHash(Object listed) throws IOException, TemplateException { + addToDataModel("listed", listed); + assertOutput( + "<#list listed as k, v>B(${k}=${v}) <#if k == 'c'>Break!<#break></#if>A(${k}=${v})<#sep>, </#list>", + "B(a=1) A(a=1), B(b=2) A(b=2), B(c=3) Break!"); + assertOutput( + "<#list listed as k, v>B(${k}=${v}) <#if k == 'c'>Continue! <#continue></#if>A(${k}=${v})<#sep>, </#list>", + "B(a=1) A(a=1), B(b=2) A(b=2), B(c=3) Continue! B(d=4) A(d=4), B(e=5) A(e=5)"); + } + + @Override + protected Configuration createConfiguration() throws Exception { + Configuration conf = super.createConfiguration(); + DefaultObjectWrapperBuilder owb = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27); + owb.setForceLegacyNonListCollections(false); + conf.setObjectWrapper(owb.build()); + return conf; + } + + /** Hides the Ex2 features of another hash */ + static class NonEx2Hash implements TemplateHashModelEx { + private final TemplateHashModelEx delegate; + + public NonEx2Hash(TemplateHashModelEx delegate) { + this.delegate = delegate; + } + + public TemplateModel get(String key) throws TemplateModelException { + return delegate.get(key); + } + + public int size() throws TemplateModelException { + return delegate.size(); + } + + public TemplateCollectionModel keys() throws TemplateModelException { + return delegate.keys(); + } + + public boolean isEmpty() throws TemplateModelException { + return delegate.isEmpty(); + } + + public TemplateCollectionModel values() throws TemplateModelException { + return delegate.values(); + } + } + +}
