Repository: freemarker Updated Branches: refs/heads/3 976ce3ac1 -> c4f96d918
FREEMARKER-55: Adding options directive, TODO: check option selected, support nested option directive. Project: http://git-wip-us.apache.org/repos/asf/freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/freemarker/commit/c4f96d91 Tree: http://git-wip-us.apache.org/repos/asf/freemarker/tree/c4f96d91 Diff: http://git-wip-us.apache.org/repos/asf/freemarker/diff/c4f96d91 Branch: refs/heads/3 Commit: c4f96d918c388b90e115447bd21dd4185cb19fd7 Parents: 976ce3a Author: Woonsan Ko <[email protected]> Authored: Fri Apr 20 23:02:23 2018 -0400 Committer: Woonsan Ko <[email protected]> Committed: Fri Apr 20 23:02:23 2018 -0400 ---------------------------------------------------------------------- .../AbstractFormTemplateDirectiveModel.java | 8 + .../spring/model/form/FormTemplateScope.java | 68 ++++++++ .../form/OptionsTemplateDirectiveModel.java | 160 +++++++++++++++++++ .../form/SelectTemplateDirectiveModel.java | 13 +- .../SpringFormTemplateCallableHashModel.java | 1 + .../example/mvc/users/UserController.java | 15 ++ .../form/SelectTemplateDirectiveModelTest.java | 26 ++- .../model/form/select-directive-usages.f3ah | 33 +++- 8 files changed, 314 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractFormTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractFormTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractFormTemplateDirectiveModel.java index 00c9f42..83f078e 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractFormTemplateDirectiveModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractFormTemplateDirectiveModel.java @@ -25,6 +25,7 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.freemarker.core.CustomStateKey; import org.apache.freemarker.core.TemplateException; import org.apache.freemarker.spring.model.AbstractSpringTemplateDirectiveModel; import org.springframework.util.ObjectUtils; @@ -34,6 +35,13 @@ import org.springframework.util.ObjectUtils; */ abstract class AbstractFormTemplateDirectiveModel extends AbstractSpringTemplateDirectiveModel { + protected static final CustomStateKey<FormTemplateScope> FORM_TEMPLATE_SCOPE_KEY = new CustomStateKey<FormTemplateScope>() { + @Override + protected FormTemplateScope create() { + return new FormTemplateScope(); + } + }; + protected AbstractFormTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { super(request, response); } http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateScope.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateScope.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateScope.java new file mode 100644 index 0000000..822eb00 --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateScope.java @@ -0,0 +1,68 @@ +/* + * 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 org.apache.freemarker.spring.model.form; + +/** + * Form related <code>TemplateCallableModel</code> execution context. + */ +public class FormTemplateScope { + + /** + * The <code>TagOutputter</code> instance in the current <code>TemplateCallableModel</code> execution context. + */ + private TagOutputter currentTagOutputter; + + /** + * The <code>SelectTemplateDirectiveModel</code> instance in the current <code>TemplateCallableModel</code> execution context. + */ + private SelectTemplateDirectiveModel currentSelectDirective; + + /** + * Return the <code>TagOutputter</code> instance in the current <code>TemplateCallableModel</code> execution context. + * @return the <code>TagOutputter</code> instance in the current <code>TemplateCallableModel</code> execution context + */ + public TagOutputter getCurrentTagOutputter() { + return currentTagOutputter; + } + + /** + * Set the <code>TagOutputter</code> instance in the current <code>TemplateCallableModel</code> execution context. + * @param currentTagOutputter the <code>TagOutputter</code> instance in the current <code>TemplateCallableModel</code> execution context + */ + public void setCurrentTagOutputter(TagOutputter currentTagOutputter) { + this.currentTagOutputter = currentTagOutputter; + } + + /** + * Return the <code>SelectTemplateDirectiveModel</code> instance in the current <code>TemplateCallableModel</code> execution context. + * @return the <code>SelectTemplateDirectiveModel</code> instance in the current <code>TemplateCallableModel</code> execution context + */ + public SelectTemplateDirectiveModel getCurrentSelectDirective() { + return currentSelectDirective; + } + + /** + * Set the <code>SelectTemplateDirectiveModel</code> instance in the current <code>TemplateCallableModel</code> execution context. + * @param currentSelectDirective the <code>SelectTemplateDirectiveModel</code> instance in the current <code>TemplateCallableModel</code> execution context + */ + public void setCurrentSelectDirective(SelectTemplateDirectiveModel currentSelectDirective) { + this.currentSelectDirective = currentSelectDirective; + } +} http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionsTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionsTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionsTemplateDirectiveModel.java new file mode 100644 index 0000000..4367eaf --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionsTemplateDirectiveModel.java @@ -0,0 +1,160 @@ +/* + * 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 org.apache.freemarker.spring.model.form; + +import java.io.IOException; +import java.io.Writer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.freemarker.core.CallPlace; +import org.apache.freemarker.core.Environment; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.ObjectWrapperAndUnwrapper; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.util.CallableUtils; +import org.apache.freemarker.core.util.StringToIndexMap; +import org.springframework.util.ObjectUtils; +import org.springframework.web.servlet.support.RequestContext; + +/** + * Provides a convenient <code>TemplateModel</code> that allow to supply a collection that are to be rendered + * as HTML '{@code option}' elements. + * <P> + * This directive supports the following parameters: + * <UL> + * <LI><code>items</code>: collection of option items.</LI> + * <LI> + * ... TODO ... + * </LI> + * </UL> + * </P> + * <P> + * Some valid example(s): + * </P> + * <PRE> + * ... + * </PRE> + * <P> + * <EM>Note:</EM> Unlike Spring Framework's <code><form:input /></code> JSP Tag Library, this directive + * does not support <code>htmlEscape</code> parameter. It always renders HTML's without escaping + * because it is much easier to control escaping in FreeMarker Template expressions. + * </P> + */ +class OptionsTemplateDirectiveModel extends AbstractHtmlInputElementTemplateDirectiveModel { + + public static final String NAME = "options"; + + private static final int NAMED_ARGS_OFFSET = AbstractHtmlInputElementTemplateDirectiveModel.ARGS_LAYOUT + .getPredefinedNamedArgumentsEndIndex(); + + private static final int ITEMS_PARAM_IDX = NAMED_ARGS_OFFSET; + private static final String ITEMS_PARAM_NAME = "items"; + + private static final int ITEM_VALUE_PARAM_IDX = NAMED_ARGS_OFFSET + 1; + private static final String ITEM_VALUE_PARAM_NAME = "itemValue"; + + private static final int ITEM_LABEL_PARAM_IDX = NAMED_ARGS_OFFSET + 2; + private static final String ITEM_LABEL_PARAM_NAME = "itemLabel"; + + protected static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, + StringToIndexMap.of( + AbstractHtmlInputElementTemplateDirectiveModel.ARGS_LAYOUT.getPredefinedNamedArgumentsMap(), + new StringToIndexMap.Entry(ITEMS_PARAM_NAME, ITEMS_PARAM_IDX), + new StringToIndexMap.Entry(ITEM_VALUE_PARAM_NAME, ITEM_VALUE_PARAM_IDX), + new StringToIndexMap.Entry(ITEM_LABEL_PARAM_NAME, ITEM_LABEL_PARAM_IDX)), + true); + + private TemplateModel items; + private String itemValue; + private String itemLabel; + + protected OptionsTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + @Override + public boolean isNestedContentSupported() { + return false; + } + + @Override + public ArgumentArrayLayout getDirectiveArgumentArrayLayout() { + return ARGS_LAYOUT; + } + + @Override + protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, final Environment env, + ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) + throws TemplateException, IOException { + + super.executeInternal(args, callPlace, out, env, objectWrapperAndUnwrapper, requestContext); + + items = CallableUtils.getOptionalArgument(args, ITEMS_PARAM_IDX, TemplateModel.class, this); + itemValue = CallableUtils.getOptionalStringArgument(args, ITEM_VALUE_PARAM_IDX, this); + itemLabel = CallableUtils.getOptionalStringArgument(args, ITEM_LABEL_PARAM_IDX, this); + + final FormTemplateScope formTemplateScope = env.getCustomState(FORM_TEMPLATE_SCOPE_KEY); + final SelectTemplateDirectiveModel curSelectDirective = formTemplateScope.getCurrentSelectDirective(); + + Object optionItems = null; + + if (getItems() != null) { + optionItems = objectWrapperAndUnwrapper.unwrap(getItems()); + if (optionItems != null) { + optionItems = evaluate("items", optionItems); + } + } + + if (optionItems != null) { + final String selectName = curSelectDirective.getName(); + final String valueProperty = (getItemValue() != null + ? ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) + : null); + final String labelProperty = (getItemLabel() != null + ? ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) + : null); + OptionOutputHelper optionOutHelper = new OptionOutputHelper(optionItems, curSelectDirective.getBindStatus(), + valueProperty, labelProperty) { + @Override + protected String processOptionValue(String resolvedValue) throws TemplateException { + return processFieldValue(env, selectName, resolvedValue, "option"); + } + }; + + optionOutHelper.writeOptions(formTemplateScope.getCurrentTagOutputter()); + } + } + + public TemplateModel getItems() { + return items; + } + + public String getItemValue() { + return itemValue; + } + + public String getItemLabel() { + return itemLabel; + } + +} http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModel.java index 26a170c..c405437 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModel.java @@ -109,8 +109,7 @@ class SelectTemplateDirectiveModel extends AbstractHtmlInputElementTemplateDirec @Override public boolean isNestedContentSupported() { - // TODO: should return true for options and option directive? What if there's anything other than option(s)? - return false; + return true; } @Override @@ -169,6 +168,16 @@ class SelectTemplateDirectiveModel extends AbstractHtmlInputElementTemplateDirec optionOutHelper.writeOptions(tagOut); } + final FormTemplateScope formTemplateScope = env.getCustomState(FORM_TEMPLATE_SCOPE_KEY); + try { + formTemplateScope.setCurrentTagOutputter(tagOut); + formTemplateScope.setCurrentSelectDirective(this); + callPlace.executeNestedContent(null, out, env); + } finally { + formTemplateScope.setCurrentSelectDirective(null); + formTemplateScope.setCurrentTagOutputter(null); + } + tagOut.endTag(); } http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java index 7d29c28..31e1591 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SpringFormTemplateCallableHashModel.java @@ -53,6 +53,7 @@ public final class SpringFormTemplateCallableHashModel implements TemplateHashMo modelsMap.put(ButtonTemplateDirectiveModel.NAME, new ButtonTemplateDirectiveModel(request, response)); modelsMap.put(LabelTemplateDirectiveModel.NAME, new LabelTemplateDirectiveModel(request, response)); modelsMap.put(SelectTemplateDirectiveModel.NAME, new SelectTemplateDirectiveModel(request, response)); + modelsMap.put(OptionsTemplateDirectiveModel.NAME, new OptionsTemplateDirectiveModel(request, response)); modelsMap.put(ErrorsTemplateDirectiveModel.NAME, new ErrorsTemplateDirectiveModel(request, response)); } http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java index 77fa2a9..71ba8e0 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserController.java @@ -20,6 +20,9 @@ package org.apache.freemarker.spring.example.mvc.users; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -43,6 +46,15 @@ import org.springframework.web.bind.annotation.RequestParam; @Controller public class UserController { + public static final List<String> INDOOR_SPORTS = Collections + .unmodifiableList(Arrays.asList("bowling", "gymnastics", "handball")); + + public static final List<String> OUTDOOR_SPORTS = Collections + .unmodifiableList(Arrays.asList("baseball", "football", "marathon")); + + public static final List<String> ALL_SPORTS = Collections + .unmodifiableList(Arrays.asList("bowling", "gymnastics", "handball", "baseball", "football", "marathon")); + private static final String DEFAULT_USER_LIST_VIEW_NAME = "example/users/userlist"; private static final String DEFAULT_USER_EDIT_VIEW_NAME = "example/users/useredit"; @@ -72,6 +84,9 @@ public class UserController { @RequestMapping(value = "/users/{id:\\d+}", method = RequestMethod.GET) public String getUser(@PathVariable("id") Long id, @RequestParam(value = "viewName", required = false) String viewName, Model model) { + model.addAttribute("indoorSports", INDOOR_SPORTS); + model.addAttribute("outdoorSports", OUTDOOR_SPORTS); + User user = userRepository.getUser(id); if (user != null) { http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java index 4a82c67..c07fb8d 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java @@ -20,6 +20,7 @@ package org.apache.freemarker.spring.model.form; import org.apache.freemarker.spring.example.mvc.users.User; +import org.apache.freemarker.spring.example.mvc.users.UserController; import org.apache.freemarker.spring.example.mvc.users.UserRepository; import org.junit.Before; import org.junit.Test; @@ -30,6 +31,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -61,12 +63,24 @@ public class SelectTemplateDirectiveModelTest { public void testBasicUsages() throws Exception { final Long userId = userRepository.getUserIds().iterator().next(); final User user = userRepository.getUser(userId); - mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/form/select-directive-usages") + final ResultActions resultAcctions = + mockMvc.perform(get("/users/{userId}/", userId).param("viewName", "test/model/form/select-directive-usages") .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) - .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) - .andExpect(xpath("//form[@id='form1']//select[@name='favoriteSport']//option[@value='football']").exists()) - .andExpect(xpath("//form[@id='form1']//select[@name='favoriteSport']//option[@value='handball']").exists()) - .andExpect(xpath("//form[@id='form1']//select[@name='favoriteSport']//option[@value='basketball']").exists()) - .andExpect(xpath("//form[@id='form1']//select[@name='favoriteSport']//option[@value='volleyball']").exists()); + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()); + + for (int i = 0; i < UserController.INDOOR_SPORTS.size(); i++) { + String sport = UserController.INDOOR_SPORTS.get(i); + resultAcctions.andExpect(xpath("//form[@id='form1']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); + } + + for (int i = 0; i < UserController.OUTDOOR_SPORTS.size(); i++) { + String sport = UserController.OUTDOOR_SPORTS.get(i); + resultAcctions.andExpect(xpath("//form[@id='form2']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); + } + + for (int i = 0; i < UserController.ALL_SPORTS.size(); i++) { + String sport = UserController.ALL_SPORTS.get(i); + resultAcctions.andExpect(xpath("//form[@id='form3']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); + } } } http://git-wip-us.apache.org/repos/asf/freemarker/blob/c4f96d91/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah index 7152055..0794c2c 100644 --- a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah @@ -26,8 +26,37 @@ <tr> <th>Favorite Sport:</th> <td> - <#assign sports=[ 'football', 'handball', 'basketball', 'volleyball' ] /> - <@form.select 'user.favoriteSport' items=sports /> + <@form.select 'user.favoriteSport' items=indoorSports /> + </td> + </tr> + </table> + </form> + + <h1>Form 2</h1> + <hr/> + <form id="form2"> + <table> + <tr> + <th>Favorite Sport:</th> + <td> + <@form.select 'user.favoriteSport'> + <@form.options items=outdoorSports /> + </@form.select> + </td> + </tr> + </table> + </form> + + <h1>Form 3</h1> + <hr/> + <form id="form3"> + <table> + <tr> + <th>Favorite Sport:</th> + <td> + <@form.select 'user.favoriteSport' items=indoorSports> + <@form.options items=outdoorSports /> + </@form.select> </td> </tr> </table>
