You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by wo...@apache.org on 2018/06/25 03:42:41 UTC

freemarker git commit: FREEMARKER-55 Adding checkbox directive. checked option completed

Repository: freemarker
Updated Branches:
  refs/heads/3 e8e58ffa5 -> bde40b5e5


FREEMARKER-55 Adding checkbox directive. checked option completed


Project: http://git-wip-us.apache.org/repos/asf/freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/freemarker/commit/bde40b5e
Tree: http://git-wip-us.apache.org/repos/asf/freemarker/tree/bde40b5e
Diff: http://git-wip-us.apache.org/repos/asf/freemarker/diff/bde40b5e

Branch: refs/heads/3
Commit: bde40b5e55a171b75254607b38f0acd7ece93f39
Parents: e8e58ff
Author: Woonsan Ko <wo...@apache.org>
Authored: Sun Jun 24 23:42:27 2018 -0400
Committer: Woonsan Ko <wo...@apache.org>
Committed: Sun Jun 24 23:42:27 2018 -0400

----------------------------------------------------------------------
 ...actCheckedElementTemplateDirectiveModel.java |  71 ++++++++
 .../AbstractFormTemplateDirectiveModel.java     |   6 +
 ...gleCheckedElementTemplateDirectiveModel.java |  77 +++++++++
 .../form/CheckboxTemplateDirectiveModel.java    | 161 +++++++++++++++++++
 .../SpringFormTemplateCallableHashModel.java    |   1 +
 .../spring/example/mvc/users/User.java          |  26 ++-
 .../example/mvc/users/UserRepository.java       |  20 +++
 .../CheckboxTemplateDirectiveModelTest.java     | 111 +++++++++++++
 .../model/form/checkbox-directive-usages.f3ah   |  50 ++++++
 9 files changed, 522 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractCheckedElementTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractCheckedElementTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractCheckedElementTemplateDirectiveModel.java
new file mode 100644
index 0000000..f6f27f3
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractCheckedElementTemplateDirectiveModel.java
@@ -0,0 +1,71 @@
+/*
+ * 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+
+/**
+ * Corresponds to <code>org.springframework.web.servlet.tags.form.AbstractCheckedElementTag</code>.
+ */
+abstract class AbstractCheckedElementTemplateDirectiveModel extends AbstractHtmlInputElementTemplateDirectiveModel {
+
+    protected AbstractCheckedElementTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) {
+        super(request, response);
+    }
+
+    /**
+     * Return "checkbox" or "radio".
+     */
+    protected abstract String getInputType();
+
+    protected void renderFromValue(Environment env, Object value, TagOutputter tagOut)
+            throws TemplateException, IOException {
+        renderFromValue(env, value, value, tagOut);
+    }
+
+    protected void renderFromValue(Environment env, Object item, Object value, TagOutputter tagOut)
+            throws TemplateException, IOException {
+        String displayValue = getDisplayString(value, getBindStatus());
+        tagOut.writeAttribute("value", processFieldValue(env, getName(), displayValue, getInputType()));
+
+        if (isOptionSelected(value) || (value != item && isOptionSelected(item))) {
+            tagOut.writeAttribute("checked", "checked");
+        }
+    }
+
+    protected void renderFromBoolean(Environment env, Boolean boundValue, TagOutputter tagOut)
+            throws TemplateException, IOException {
+        tagOut.writeAttribute("value", processFieldValue(env, getName(), "true", getInputType()));
+
+        if (boundValue != null && boundValue.booleanValue()) {
+            tagOut.writeAttribute("checked", "checked");
+        }
+    }
+
+    private boolean isOptionSelected(Object value) throws TemplateException {
+        return SelectableValueComparisonUtils.isEqualValueBoundTo(value, getBindStatus());
+    }
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/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 83f078e..fa0152e 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
@@ -29,6 +29,7 @@ import org.apache.freemarker.core.CustomStateKey;
 import org.apache.freemarker.core.TemplateException;
 import org.apache.freemarker.spring.model.AbstractSpringTemplateDirectiveModel;
 import org.springframework.util.ObjectUtils;
+import org.springframework.web.servlet.support.BindStatus;
 
 /**
  * Corresponds to <code>org.springframework.web.servlet.tags.form.AbstractFormTag</code>.
@@ -55,6 +56,11 @@ abstract class AbstractFormTemplateDirectiveModel extends AbstractSpringTemplate
         return displayValue;
     }
 
+    public static String getDisplayString(Object value, BindStatus bindStatus) {
+        final PropertyEditor editor = (bindStatus != null && value != null) ? bindStatus.findEditor(value.getClass()) : null;
+        return getDisplayString(value, editor);
+    }
+
     public static String getDisplayString(Object value, PropertyEditor propertyEditor) {
         if (propertyEditor != null && !(value instanceof String)) {
             try {

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractSingleCheckedElementTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractSingleCheckedElementTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractSingleCheckedElementTemplateDirectiveModel.java
new file mode 100644
index 0000000..48b9498
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractSingleCheckedElementTemplateDirectiveModel.java
@@ -0,0 +1,77 @@
+/*
+ * 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.ObjectWrapperAndUnwrapper;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * Corresponds to <code>org.springframework.web.servlet.tags.form.AbstractSingleCheckedElementTag</code>.
+ */
+abstract class AbstractSingleCheckedElementTemplateDirectiveModel extends AbstractCheckedElementTemplateDirectiveModel {
+
+    protected AbstractSingleCheckedElementTemplateDirectiveModel(HttpServletRequest request,
+            HttpServletResponse response) {
+        super(request, response);
+    }
+
+    @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);
+
+        TagOutputter tagOut = new TagOutputter(out);
+
+        tagOut.beginTag("input");
+        String id = resolveId();
+        writeOptionalAttribute(tagOut, "id", id);
+        writeOptionalAttribute(tagOut, "name", getName());
+        writeOptionalAttributes(tagOut);
+        writeAdditionalDetails(env, tagOut);
+        tagOut.endTag();
+
+        Object resolvedLabel = evaluate("label", getLabel());
+
+        if (resolvedLabel != null) {
+            tagOut.beginTag("label");
+            tagOut.writeAttribute("for", id);
+            tagOut.appendValue(getDisplayString(resolvedLabel, getBindStatus()));
+            tagOut.endTag();
+        }
+    }
+
+    public abstract Object getValue();
+
+    public abstract Object getLabel();
+
+    protected abstract void writeAdditionalDetails(Environment env, TagOutputter tagOut) throws TemplateException, IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModel.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModel.java
new file mode 100644
index 0000000..95da416
--- /dev/null
+++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModel.java
@@ -0,0 +1,161 @@
+/*
+ * 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.web.bind.WebDataBinder;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * Provides <code>TemplateModel</code> for data-binding-aware HTML '{@code <input type="checkbox"/>}' element.
+ * This tag is provided for completeness if the application relies on a
+ * <code>org.springframework.web.servlet.support.RequestDataValueProcessor</code>.
+ * <P>
+ * This directive supports the following parameters:
+ * <UL>
+ * <LI>
+ *   ... TODO ...
+ * </LI>
+ * </UL>
+ * </P>
+ * <P>
+ * Some valid example(s):
+ * </P>
+ * <PRE>
+ *   &lt;@form.checkbox 'user.receiveNewsletter' /&gt;
+ * </PRE>
+ * <P>
+ * <EM>Note:</EM> Unlike Spring Framework's <code>&lt;form:button /&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 CheckboxTemplateDirectiveModel extends AbstractSingleCheckedElementTemplateDirectiveModel {
+
+    public static final String NAME = "checkbox";
+
+    private static final int NAMED_ARGS_OFFSET = AbstractHtmlInputElementTemplateDirectiveModel.ARGS_LAYOUT
+            .getPredefinedNamedArgumentsEndIndex();
+
+    private static final int VALUE_PARAM_IDX = NAMED_ARGS_OFFSET;
+    private static final String VALUE_PARAM_NAME = "value";
+
+    private static final int LABEL_PARAM_IDX = NAMED_ARGS_OFFSET + 1;
+    private static final String LABEL_PARAM_NAME = "label";
+
+    protected static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false,
+            StringToIndexMap.of(AbstractHtmlInputElementTemplateDirectiveModel.ARGS_LAYOUT.getPredefinedNamedArgumentsMap(),
+                    new StringToIndexMap.Entry(VALUE_PARAM_NAME, VALUE_PARAM_IDX),
+                    new StringToIndexMap.Entry(LABEL_PARAM_NAME, LABEL_PARAM_IDX)),
+            true);
+
+    private String value;
+    private String label;
+
+    protected CheckboxTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) {
+        super(request, response);
+    }
+
+    @Override
+    public ArgumentArrayLayout getDirectiveArgumentArrayLayout() {
+        return ARGS_LAYOUT;
+    }
+
+    @Override
+    public boolean isNestedContentSupported() {
+        return false;
+    }
+
+    @Override
+    protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env,
+            ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext)
+            throws TemplateException, IOException {
+        value = CallableUtils.getOptionalStringArgument(args, VALUE_PARAM_IDX, this);
+        label = CallableUtils.getOptionalStringArgument(args, LABEL_PARAM_IDX, this);
+
+        super.executeInternal(args, callPlace, out, env, objectWrapperAndUnwrapper, requestContext);
+    }
+
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public String getLabel() {
+        return label;
+    }
+
+    @Override
+    protected void writeAdditionalDetails(Environment env, TagOutputter tagOut) throws TemplateException, IOException {
+        tagOut.writeAttribute("type", getInputType());
+
+        Object boundValue = getBindStatus().getValue();
+        Class<?> valueType = getBindStatus().getValueType();
+
+        if (Boolean.class == valueType || boolean.class == valueType) {
+            if (boundValue instanceof String) {
+                boundValue = Boolean.valueOf((String) boundValue);
+            }
+
+            final Boolean booleanValue = (boundValue != null) ? (Boolean) boundValue : Boolean.FALSE;
+            renderFromBoolean(env, booleanValue, tagOut);
+        } else {
+            Object value = getValue();
+
+            if (value == null) {
+                throw new IllegalArgumentException("value attribute is missing. It's required to bind to a non-boolean value(s).");
+            }
+
+            Object resolvedValue = (value instanceof String ? evaluate("value", value) : value);
+            renderFromValue(env, resolvedValue, tagOut);
+        }
+
+        if (!isDisabled()) {
+            // Spring Web MVC requires to render a hidden input as a 'field was present' marker.
+            tagOut.beginTag("input");
+            tagOut.writeAttribute("type", "hidden");
+            String name = WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName();
+            tagOut.writeAttribute("name", name);
+            tagOut.writeAttribute("value", processFieldValue(env, name, "on", getInputType()));
+            tagOut.endTag();
+        }
+    }
+
+    @Override
+    protected String getInputType() {
+        return "checkbox";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/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 b460c4c..aaab1f1 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
@@ -56,6 +56,7 @@ public final class SpringFormTemplateCallableHashModel implements TemplateHashMo
         modelsMap.put(OptionsTemplateDirectiveModel.NAME, new OptionsTemplateDirectiveModel(request, response));
         modelsMap.put(OptionTemplateDirectiveModel.NAME, new OptionTemplateDirectiveModel(request, response));
         modelsMap.put(ErrorsTemplateDirectiveModel.NAME, new ErrorsTemplateDirectiveModel(request, response));
+        modelsMap.put(CheckboxTemplateDirectiveModel.NAME, new CheckboxTemplateDirectiveModel(request, response));
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
index 028f920..56330e6 100644
--- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/User.java
@@ -31,6 +31,8 @@ public class User {
     private Date birthDate;
     private String description;
     private String favoriteSport;
+    private boolean receiveNewsletter;
+    private String[] favoriteFood;
 
     public User() {
     }
@@ -99,10 +101,32 @@ public class User {
         this.favoriteSport = favoriteSport;
     }
 
+    public boolean isReceiveNewsletter() {
+        return receiveNewsletter;
+    }
+
+    public void setReceiveNewsletter(boolean receiveNewsletter) {
+        this.receiveNewsletter = receiveNewsletter;
+    }
+
+    public String[] getFavoriteFood() {
+        if (favoriteFood == null) {
+            return null;
+        }
+
+        String[] cloned = new String[favoriteFood.length];
+        System.arraycopy(favoriteFood, 0, cloned, 0, favoriteFood.length);
+        return cloned;
+    }
+
+    public void setFavoriteFood(String[] favoriteFood) {
+        this.favoriteFood = favoriteFood;
+    }
+
     @Override
     public String toString() {
         return super.toString() + " {id=" + id + ", firstName='" + firstName + "', lastName='" + lastName + "', email='"
                 + email + "', birthDate='" + birthDate + "', description='" + description + "', favoriteSport='"
-                + favoriteSport + "'}";
+                + favoriteSport + "', receiveNewsletter=" + receiveNewsletter + ", favoriteFood=" + favoriteFood + "}";
     }
 }

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
index e2a2283..57af289 100644
--- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/example/mvc/users/UserRepository.java
@@ -58,6 +58,8 @@ public class UserRepository {
         user.setDescription("Lorem ipsum dolor sit amet, \r\nconsectetur adipiscing elit, \r\n"
                 + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.");
         user.setFavoriteSport("baseball");
+        user.setReceiveNewsletter(false);
+        user.setFavoriteFood(new String[] { "Sandwich", "Spaghetti" });
         usersMap.put(id, user);
 
         id = 102L;
@@ -74,6 +76,8 @@ public class UserRepository {
         user.setDescription("Ut enim ad minim veniam, \r\n"
                 + "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.");
         user.setFavoriteSport("marathon");
+        user.setReceiveNewsletter(true);
+        user.setFavoriteFood(new String[] { "Sandwich", "Sushi" });
         usersMap.put(id, user);
     }
 
@@ -95,6 +99,20 @@ public class UserRepository {
         return null;
     }
 
+    public synchronized User getUserByEmail(final String email) {
+        if (email == null) {
+            throw new IllegalArgumentException("E-Mail must be non-null.");
+        }
+
+        for (User user : usersMap.values()) {
+            if (email.equals(user.getEmail())) {
+                return cloneUser(user, user.getId());
+            }
+        }
+
+        return null;
+    }
+
     public synchronized User addOrUpdateUser(final User user) {
         final Long id = user.getId();
         User newUser = cloneUser(user, id);
@@ -120,6 +138,8 @@ public class UserRepository {
         clone.setBirthDate(source.getBirthDate());
         clone.setDescription(source.getDescription());
         clone.setFavoriteSport(source.getFavoriteSport());
+        clone.setReceiveNewsletter(source.isReceiveNewsletter());
+        clone.setFavoriteFood(source.getFavoriteFood());
         return clone;
     }
 }

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModelTest.java
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModelTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModelTest.java
new file mode 100644
index 0000000..5d74d8c
--- /dev/null
+++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxTemplateDirectiveModelTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.User;
+import org.apache.freemarker.spring.example.mvc.users.UserRepository;
+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.get;
+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 CheckboxTemplateDirectiveModelTest {
+
+    @Autowired
+    private WebApplicationContext wac;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    private MockMvc mockMvc;
+
+    @Before
+    public void setUp() {
+        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
+    }
+
+    @Test
+    public void testSingleCheckboxWithNotChecked() throws Exception {
+        final User user = userRepository.getUserByEmail("john@example.com");
+        mockMvc.perform(get("/users/{userId}/", user.getId()).param("viewName", "test/model/form/checkbox-directive-usages")
+                .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='receiveNewsletter' and @name='receiveNewsletter']/@value").string("true"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='receiveNewsletter' and @name='receiveNewsletter']/@checked").doesNotExist())
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' and @name='_receiveNewsletter']/@value").string("on"));
+    }
+
+    @Test
+    public void testSingleCheckboxWithChecked() throws Exception {
+        final User user = userRepository.getUserByEmail("jane@example.com");
+        mockMvc.perform(get("/users/{userId}/", user.getId()).param("viewName", "test/model/form/checkbox-directive-usages")
+                .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='receiveNewsletter' and @name='receiveNewsletter']/@value").string("true"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='receiveNewsletter' and @name='receiveNewsletter']/@checked").string("checked"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' and @name='_receiveNewsletter']/@value").string("on"));
+    }
+
+    @Test
+    public void testMultipleCheckboxes101() throws Exception {
+        final User user = userRepository.getUserByEmail("john@example.com");
+        mockMvc.perform(get("/users/{userId}/", user.getId()).param("viewName", "test/model/form/checkbox-directive-usages")
+                .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sandwich']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Spaghetti']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sushi']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' and @name='_favoriteFood' and @value='on']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sandwich']/@checked").string("checked"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Spaghetti']/@checked").string("checked"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sushi']/@checked").doesNotExist());
+    }
+
+    @Test
+    public void testMultipleCheckboxes102() throws Exception {
+        final User user = userRepository.getUserByEmail("jane@example.com");
+        mockMvc.perform(get("/users/{userId}/", user.getId()).param("viewName", "test/model/form/checkbox-directive-usages")
+                .accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                .andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sandwich']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Spaghetti']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sushi']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' and @name='_favoriteFood' and @value='on']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sandwich']/@checked").string("checked"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Spaghetti']/@checked").doesNotExist())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' and @id='favoriteFood' and @name='favoriteFood' and @value='Sushi']/@checked").string("checked"));
+    }
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/bde40b5e/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkbox-directive-usages.f3ah
----------------------------------------------------------------------
diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkbox-directive-usages.f3ah b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkbox-directive-usages.f3ah
new file mode 100644
index 0000000..e1d395a
--- /dev/null
+++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkbox-directive-usages.f3ah
@@ -0,0 +1,50 @@
+<#--
+  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 id="form1">
+    <table>
+      <tr>
+        <th>User:</th>
+        <td>
+          ${user.firstName} ${user.lastName} (${user.email})
+        </td>
+      </tr>
+      <tr>
+        <th>Receive Newsletter?</th>
+        <td>
+          <@form.checkbox "user.receiveNewsletter" />
+        </td>
+      </tr>
+      <tr>
+        <th>Favorite Food</th>
+        <td>
+          <@form.checkbox "user.favoriteFood" value="Sandwich" />Sandwich
+          <@form.checkbox "user.favoriteFood" value="Spaghetti" />Spaghetti
+          <@form.checkbox "user.favoriteFood" value="Sushi" />Sushi
+        </td>
+      </tr>
+    </table>
+  </form>
+
+</body>
+</html>