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>
+ *   &lt;@form.errors '*'; messages&gt;
+ *     &lt;ul&gt;
+ *       &lt;#list messages as message&gt;
+ *         &lt;li&gt;${message}&lt;/li&gt;
+ *       &lt;/#list&gt;
+ *     &lt;/ul&gt;
+ *   &lt;/@form.errors&gt;
+ *
+ *   &lt;!-- SNIP --&gt;
+ *
+ *   &lt;@form.errors 'firstName'; messages /&gt;
+ *
+ *   &lt;!-- SNIP --&gt;
+ *
+ *   &lt;@form.errors 'email' cssClass="errorEmail"; messages /&gt;
+ * </PRE>
+ * <P>
+ * <EM>Note:</EM> Unlike Spring Framework's <code>&lt;form:errors /&gt;</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>

Reply via email to