Repository: incubator-freemarker Updated Branches: refs/heads/3 04c0f1a54 -> fe83bc9f7
FREEMARKER-55: Adding form.errors directive Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/fe83bc9f Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/fe83bc9f Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/fe83bc9f Branch: refs/heads/3 Commit: fe83bc9f78cc264b4eab671c7ef975c5d4d32858 Parents: 04c0f1a Author: Woonsan Ko <woon...@apache.org> Authored: Mon Jan 29 16:34:41 2018 -0500 Committer: Woonsan Ko <woon...@apache.org> Committed: Mon Jan 29 16:34:41 2018 -0500 ---------------------------------------------------------------------- FM3-CHANGE-LOG.txt | 1 + .../form/ErrorsTemplateDirectiveModel.java | 214 +++++++++++++++++++ .../model/form/FormTemplateDirectiveModel.java | 4 + .../SpringFormTemplateCallableHashModel.java | 1 + .../example/mvc/users/UserController.java | 10 + .../form/ErrorsTemplateDirectiveModelTest.java | 74 +++++++ .../model/form/errors-directive-usages.ftlh | 67 ++++++ 7 files changed, 371 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/FM3-CHANGE-LOG.txt ---------------------------------------------------------------------- diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt index 078c416..a4dfb51 100644 --- a/FM3-CHANGE-LOG.txt +++ b/FM3-CHANGE-LOG.txt @@ -529,6 +529,7 @@ models by default like FreemarkerServlet does. - <form:hidden ... /> : Replaced by <@form.hidden ... /> directive. - <form:button ... /> : Replaced by <@form.button ... /> directive. - <form:label ... /> : Replaced by <@form.label ... /> directive. + - <form:errors ... /> : Replaced by <@form.errors ... /> directive. Core / Miscellaneous .................... http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModel.java new file mode 100644 index 0000000..80ada12 --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModel.java @@ -0,0 +1,214 @@ +/* + * 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.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +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.model.impl.SimpleCollection; +import org.apache.freemarker.core.util.CallableUtils; +import org.apache.freemarker.core.util.StringToIndexMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.support.BindStatus; +import org.springframework.web.servlet.support.RequestContext; + +/** + * Provides <code>TemplateModel</code> for displaying errors for a particular field or object. + * <P> + * This directive supports the following parameters: + * <UL> + * <LI><code>path</code>: The first positional parameter pointing to the bean or bean property to bind status information for.</LI> + * <LI> + * ... TODO ... + * </LI> + * </UL> + * </P> + * <P> + * Some valid example(s): + * </P> + * <PRE> + * <@form.errors '*'; messages> + * <ul> + * <#list messages as message> + * <li>${message}</li> + * </#list> + * </ul> + * </@form.errors> + * + * <!-- SNIP --> + * + * <@form.errors 'firstName'; messages /> + * + * <!-- SNIP --> + * + * <@form.errors 'email' cssClass="errorEmail"; messages /> + * </PRE> + * <P> + * <EM>Note:</EM> Unlike Spring Framework's <code><form:errors /></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 ErrorsTemplateDirectiveModel extends AbstractHtmlElementTemplateDirectiveModel { + + public static final String NAME = "errors"; + + private static final String DEFAULT_ELEMENT = "span"; + + private static final String DEFAULT_DELIMITER = "<br/>"; + + private static final int NAMED_ARGS_OFFSET = AbstractHtmlElementTemplateDirectiveModel.ARGS_LAYOUT + .getPredefinedNamedArgumentsEndIndex(); + + private static final int ELEMENT_PARAM_IDX = NAMED_ARGS_OFFSET; + private static final String ELEMENT_PARAM_NAME = "element"; + + private static final int DELIMITER_PARAM_IDX = NAMED_ARGS_OFFSET + 1; + private static final String DELIMITER_PARAM_NAME = "delimiter"; + + protected static final ArgumentArrayLayout ARGS_LAYOUT = + ArgumentArrayLayout.create( + 1, + false, + StringToIndexMap.of(AbstractHtmlElementTemplateDirectiveModel.ARGS_LAYOUT.getPredefinedNamedArgumentsMap(), + new StringToIndexMap.Entry(ELEMENT_PARAM_NAME, ELEMENT_PARAM_IDX), + new StringToIndexMap.Entry(DELIMITER_PARAM_NAME, DELIMITER_PARAM_IDX) + ), + true); + + private String element = "span"; + + private String delimiter = "<br/>"; + + protected ErrorsTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + @Override + public ArgumentArrayLayout getDirectiveArgumentArrayLayout() { + return ARGS_LAYOUT; + } + + @Override + public boolean isNestedContentSupported() { + return true; + } + + @Override + protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env, + ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) + throws TemplateException, IOException { + super.executeInternal(args, callPlace, out, env, objectWrapperAndUnwrapper, requestContext); + + if (!shouldRender()) { + return; + } + + String param = CallableUtils.getOptionalStringArgument(args, ELEMENT_PARAM_IDX, this); + element = (StringUtils.hasText(param)) ? param.trim() : DEFAULT_ELEMENT; + + param = CallableUtils.getOptionalStringArgument(args, DELIMITER_PARAM_IDX, this); + delimiter = (StringUtils.hasText(param)) ? param : DEFAULT_DELIMITER; + + String nestedContent = null; + + try { + List<String> messages = new ArrayList<String>(); + messages.addAll(Arrays.asList(getBindStatus().getErrorMessages())); + SimpleCollection messagesModel = new SimpleCollection(messages, objectWrapperAndUnwrapper); + final TemplateModel[] nestedContentArgs = new TemplateModel[] { messagesModel }; + + StringWriter nestedOut = new StringWriter(1024); + callPlace.executeNestedContent(nestedContentArgs, nestedOut, env); + nestedContent = nestedOut.toString(); + } finally { + if (StringUtils.hasText(nestedContent)) { + out.write(nestedContent); + } else { + TagOutputter tagOut = new TagOutputter(out); + renderDefaultContent(tagOut); + } + } + } + + public String getElement() { + return element; + } + + public String getDelimiter() { + return delimiter; + } + + protected void renderDefaultContent(final TagOutputter tagOut) throws TemplateException, IOException { + tagOut.beginTag(getElement()); + writeDefaultAttributes(tagOut); + String delim = ObjectUtils.getDisplayString(evaluate("delimiter", getDelimiter())); + String[] errorMessages = getBindStatus().getErrorMessages(); + + for (int i = 0; i < errorMessages.length; i++) { + String errorMessage = errorMessages[i]; + + if (i > 0) { + tagOut.appendValue(delim); + } + + tagOut.appendValue(getDisplayString(errorMessage)); + } + + tagOut.endTag(); + } + + @Override + protected String autogenerateId() throws TemplateException { + String path = getPropertyPath(); + + if ("".equals(path) || "*".equals(path)) { + path = (String) getRequest().getAttribute(FormTemplateDirectiveModel.MODEL_ATTRIBUTE_VARIABLE_NAME); + } + + return StringUtils.deleteAny(path, "[]") + ".errors"; + } + + @Override + protected String getName() throws TemplateException { + // suppress the 'id' and 'name' attribute. + return null; + } + + protected boolean shouldRender() throws TemplateException { + final BindStatus bindStatus = getBindStatus(); + return (bindStatus != null) && bindStatus.isError(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java index c8b72c7..542ef03 100644 --- a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/FormTemplateDirectiveModel.java @@ -138,6 +138,8 @@ class FormTemplateDirectiveModel extends AbstractHtmlElementTemplateDirectiveMod true ); + static final String MODEL_ATTRIBUTE_VARIABLE_NAME = FormTemplateDirectiveModel.class.getName() + ".modelAttribute"; + private static final String FORM_TAG_NAME = "form"; private static final String INPUT_TAG_NAME = "input"; @@ -234,9 +236,11 @@ class FormTemplateDirectiveModel extends AbstractHtmlElementTemplateDirectiveMod .wrap(newNestedPath); try { + getRequest().setAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, modelAttribute); springTemplateModel.setNestedPathModel(newNestedPathModel); callPlace.executeNestedContent(null, out, env); } finally { + getRequest().removeAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME); springTemplateModel.setNestedPathModel(prevNestedPathModel); tagOut.endTag(); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/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 dc82a41..3c1b621 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 @@ -52,6 +52,7 @@ public final class SpringFormTemplateCallableHashModel implements TemplateHashMo modelsMap.put(TextareaTemplateDirectiveModel.NAME, new TextareaTemplateDirectiveModel(request, response)); modelsMap.put(ButtonTemplateDirectiveModel.NAME, new ButtonTemplateDirectiveModel(request, response)); modelsMap.put(LabelTemplateDirectiveModel.NAME, new LabelTemplateDirectiveModel(request, response)); + modelsMap.put(ErrorsTemplateDirectiveModel.NAME, new ErrorsTemplateDirectiveModel(request, response)); } @Override http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/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 5b1799e..77fa2a9 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 @@ -94,6 +94,16 @@ public class UserController { new String[] { "user.error.invalid.email" }, new Object[] { user.getEmail() }, "E-Mail is blank.")); } + if (!StringUtils.hasText(user.getFirstName())) { + bindingResult.addError(new FieldError("user", "firstName", user.getFirstName(), true, + new String[] { "user.error.invalid.firstName" }, new Object[] { user.getFirstName() }, "First name is blank.")); + } + + if (!StringUtils.hasText(user.getLastName())) { + bindingResult.addError(new FieldError("user", "lastName", user.getLastName(), true, + new String[] { "user.error.invalid.lastName" }, new Object[] { user.getLastName() }, "Last name is blank.")); + } + // No saving for now... return (StringUtils.hasText(viewName)) ? viewName : DEFAULT_USER_EDIT_VIEW_NAME; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModelTest.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModelTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModelTest.java new file mode 100644 index 0000000..3ee929e --- /dev/null +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/ErrorsTemplateDirectiveModelTest.java @@ -0,0 +1,74 @@ +/* + * 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 org.apache.freemarker.spring.example.mvc.users.UserRepository; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +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.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath; + +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration("classpath:META-INF/web-resources") +@ContextConfiguration(locations = { "classpath:org/apache/freemarker/spring/example/mvc/users/users-mvc-context.xml" }) +public class ErrorsTemplateDirectiveModelTest { + + @Autowired + private WebApplicationContext wac; + + @Autowired + private UserRepository userRepository; + + private MockMvc mockMvc; + + @Before + public void setUp() { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + public void testBasicUsages() throws Exception { + final Long userId = userRepository.getUserIds().iterator().next(); + mockMvc.perform(post("/users/", userId).param("viewName", "test/model/form/errors-directive-usages") + .param("firstName", "").param("lastName", "").param("email", "") + .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print()) + .andExpect(xpath("string(//form[@id='form1']//div[@id='formErrors']/ul)").string(Matchers.containsString("First name"))) + .andExpect(xpath("string(//form[@id='form1']//div[@id='formErrors']/ul)").string(Matchers.containsString("Last name"))) + .andExpect(xpath("string(//form[@id='form1']//div[@id='formErrors']/ul)").string(Matchers.containsString("E-Mail"))) + .andExpect(xpath("//form[@id='form1']//span[@class='errorFirstName']/text()").string(Matchers.containsString("First name"))) + .andExpect(xpath("//form[@id='form1']//span[@class='errorLastName']/text()").string(Matchers.containsString("Last name"))) + .andExpect(xpath("//form[@id='form1']//span[@class='errorEmail']/text()").string(Matchers.containsString("E-Mail"))); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/fe83bc9f/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/errors-directive-usages.ftlh ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/errors-directive-usages.ftlh b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/errors-directive-usages.ftlh new file mode 100644 index 0000000..8b79f7e --- /dev/null +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/errors-directive-usages.ftlh @@ -0,0 +1,67 @@ +<#-- + 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. +--> +<html> +<body> + + <h1>Form 1</h1> + <hr/> + + <@form.form 'user' id="form1"> + <div id="formErrors"> + <@form.errors '*'; messages> + <ul> + <#list messages as message> + <li>${message}</li> + </#list> + </ul> + </@form.errors> + </div> + <table> + <tr> + <th> + <@form.label 'firstName'>First name:</@form.label> + </th> + <td> + <@form.input 'firstName' /> + <@form.errors 'firstName' cssClass="errorFirstName"; messages /> + </td> + </tr> + <tr> + <th> + <@form.label 'lastName'>Last name:</@form.label> + </th> + <td> + <@form.input 'lastName' /> + <@form.errors 'lastName' cssClass="errorLastName"; messages /> + </td> + </tr> + <tr> + <th> + <@form.label 'email'>E-Mail:</@form.label> + </th> + <td> + <@form.input 'email' /> + <@form.errors 'email' cssClass="errorEmail"; messages /> + </td> + </tr> + </table> + </@form.form> + +</body> +</html>