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


FREEMARKER-55: Adding checkboxes directive.


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

Branch: refs/heads/3
Commit: cb072d7dc78e67955f16f560529a4e07f33c1b54
Parents: bde40b5
Author: Woonsan Ko <woon...@apache.org>
Authored: Fri Jul 20 19:25:06 2018 -0400
Committer: Woonsan Ko <woon...@apache.org>
Committed: Fri Jul 20 19:25:06 2018 -0400

----------------------------------------------------------------------
 FM3-CHANGE-LOG.txt                              |   5 +
 ...actCheckedElementTemplateDirectiveModel.java |   5 +
 ...aBoundFormElementTemplateDirectiveModel.java |  10 +-
 ...ltiCheckedElementTemplateDirectiveModel.java | 271 +++++++++++++++++++
 ...gleCheckedElementTemplateDirectiveModel.java |   4 +-
 .../form/ButtonTemplateDirectiveModel.java      |   2 +-
 .../form/CheckboxesTemplateDirectiveModel.java  | 144 ++++++++++
 .../form/ErrorsTemplateDirectiveModel.java      |   4 +-
 .../model/form/FormTemplateDirectiveModel.java  |   2 +-
 .../form/HiddenInputTemplateDirectiveModel.java |   2 +-
 .../model/form/InputTemplateDirectiveModel.java |   2 +-
 .../model/form/LabelTemplateDirectiveModel.java |   2 +-
 .../form/OptionTemplateDirectiveModel.java      |   4 +-
 .../form/SelectTemplateDirectiveModel.java      |   2 +-
 .../SpringFormTemplateCallableHashModel.java    |   1 +
 .../spring/model/form/TagIdGenerationUtils.java |  52 ++++
 .../spring/model/form/TagOutputter.java         |  10 +-
 .../form/TextareaTemplateDirectiveModel.java    |   2 +-
 .../CheckboxTemplateDirectiveModelTest.java     |  32 +--
 .../CheckboxesTemplateDirectiveModelTest.java   | 107 ++++++++
 .../model/form/checkboxes-directive-usages.f3ah |  76 ++++++
 21 files changed, 702 insertions(+), 37 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/FM3-CHANGE-LOG.txt
----------------------------------------------------------------------
diff --git a/FM3-CHANGE-LOG.txt b/FM3-CHANGE-LOG.txt
index 245be21..2ccbb91 100644
--- a/FM3-CHANGE-LOG.txt
+++ b/FM3-CHANGE-LOG.txt
@@ -552,6 +552,11 @@ models by default like FreemarkerServlet does.
   - <form:button ... /> : Replaced by <@form.button ... /> directive.
   - <form:label ... /> : Replaced by <@form.label ... /> directive.
   - <form:errors ... /> : Replaced by <@form.errors ... /> directive.
+  - <form:select ... /> : Replaced by <@form.select ... /> directive.
+  - <form:option ... /> : Replaced by <@form.option ... /> directive.
+  - <form:options ... /> : Replaced by <@form.options ... /> directive.
+  - <form:checkbox ... /> : Replaced by <@form.checkbox ... /> directive.
+  - <form:checkboxes ... /> : Replaced by <@form.checkboxes ... /> directive.
 
 Core / Miscellaneous
 ....................

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/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
index f6f27f3..f005546 100644
--- 
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
@@ -65,6 +65,11 @@ abstract class AbstractCheckedElementTemplateDirectiveModel 
extends AbstractHtml
         }
     }
 
+    @Override
+    protected String autogenerateId(Environment env) throws TemplateException {
+        return TagIdGenerationUtils.getNextId(env, super.autogenerateId(env));
+    }
+
     private boolean isOptionSelected(Object value) throws TemplateException {
         return SelectableValueComparisonUtils.isEqualValueBoundTo(value, 
getBindStatus());
     }

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractDataBoundFormElementTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractDataBoundFormElementTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractDataBoundFormElementTemplateDirectiveModel.java
index 8867be0..159c9c5 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractDataBoundFormElementTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractDataBoundFormElementTemplateDirectiveModel.java
@@ -51,7 +51,7 @@ abstract class 
AbstractDataBoundFormElementTemplateDirectiveModel extends Abstra
 
     private static final String NAME_ATTR_NAME = "name";
 
-    private static final String ID_PARAM_NAME = ID_ATTR_NAME;
+    protected static final String ID_PARAM_NAME = ID_ATTR_NAME;
 
     protected static final ArgumentArrayLayout ARGS_LAYOUT =
             ArgumentArrayLayout.create(
@@ -102,11 +102,11 @@ abstract class 
AbstractDataBoundFormElementTemplateDirectiveModel extends Abstra
     }
 
     protected void writeDefaultAttributes(TagOutputter tagOut) throws 
TemplateException, IOException {
-        writeOptionalAttribute(tagOut, ID_ATTR_NAME, resolveId());
+        writeOptionalAttribute(tagOut, ID_ATTR_NAME, 
resolveId(tagOut.getEnvironment()));
         writeOptionalAttribute(tagOut, NAME_ATTR_NAME, getName());
     }
 
-    protected String resolveId() throws TemplateException {
+    protected String resolveId(Environment env) throws TemplateException {
         Object id = evaluate(ID_PARAM_NAME, getId());
 
         if (id != null) {
@@ -114,10 +114,10 @@ abstract class 
AbstractDataBoundFormElementTemplateDirectiveModel extends Abstra
             return (StringUtils.hasText(idString) ? idString : null);
         }
 
-        return autogenerateId();
+        return autogenerateId(env);
     }
 
-    protected String autogenerateId() throws TemplateException {
+    protected String autogenerateId(Environment env) throws TemplateException {
         return StringUtils.deleteAny(getName(), "[]");
     }
 

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractMultiCheckedElementTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractMultiCheckedElementTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractMultiCheckedElementTemplateDirectiveModel.java
new file mode 100644
index 0000000..1335ff7
--- /dev/null
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/AbstractMultiCheckedElementTemplateDirectiveModel.java
@@ -0,0 +1,271 @@
+/*
+ * 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 java.util.Collection;
+import java.util.Map;
+
+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.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateHashModelEx.KeyValuePair;
+import 
org.apache.freemarker.core.model.TemplateHashModelEx.KeyValuePairIterator;
+import org.apache.freemarker.core.model.TemplateIterableModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelIterator;
+import org.apache.freemarker.core.util.CallableUtils;
+import org.apache.freemarker.core.util.StringToIndexMap;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.PropertyAccessorFactory;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.support.RequestContext;
+
+/**
+ * Corresponds to 
<code>org.springframework.web.servlet.tags.form.AbstractMultiCheckedElementTag</code>.
+ */
+abstract class AbstractMultiCheckedElementTemplateDirectiveModel extends 
AbstractCheckedElementTemplateDirectiveModel {
+
+    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";
+
+    private static final int ENCLOSING_ELEMENT_PARAM_IDX = NAMED_ARGS_OFFSET + 
3;
+    private static final String ENCLOSING_ELEMENT_PARAM_NAME = "element";
+
+    private static final int ITEM_ELEMENT_DELIMITER_PARAM_IDX = 
NAMED_ARGS_OFFSET + 4;
+    private static final String ITEM_ELEMENT_DELIMITER_PARAM_NAME = 
"delimiter";
+
+    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),
+                    new StringToIndexMap.Entry(ENCLOSING_ELEMENT_PARAM_NAME, 
ENCLOSING_ELEMENT_PARAM_IDX),
+                    new 
StringToIndexMap.Entry(ITEM_ELEMENT_DELIMITER_PARAM_NAME, 
ITEM_ELEMENT_DELIMITER_PARAM_IDX)),
+            true);
+
+    private static final String DEFAULT_ENCLOSING_ELEMENT_NAME = "span";
+
+    private TemplateModel items;
+    private String itemValue;
+    private String itemLabel;
+    private String enclosingElementName;
+    private String itemElementDelimiter;
+
+    protected 
AbstractMultiCheckedElementTemplateDirectiveModel(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);
+
+        items = CallableUtils.getArgument(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);
+        enclosingElementName = CallableUtils.getOptionalStringArgument(args, 
ENCLOSING_ELEMENT_PARAM_IDX,
+                DEFAULT_ENCLOSING_ELEMENT_NAME, this);
+        itemElementDelimiter = CallableUtils.getOptionalStringArgument(args, 
ITEM_ELEMENT_DELIMITER_PARAM_IDX, this);
+
+        final TagOutputter tagOut = new TagOutputter(env, out);
+
+        final String itemValue = getItemValue();
+        final String itemLabel = getItemLabel();
+        final String valueProperty = (itemValue != null ? 
ObjectUtils.getDisplayString(evaluate("itemValue", itemValue))
+                : null);
+        final String labelProperty = (itemLabel != null ? 
ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel))
+                : null);
+
+        if (items instanceof TemplateIterableModel) {
+            int itemIndex = 0;
+
+            for (TemplateModelIterator it = ((TemplateIterableModel) 
items).iterator(); it.hasNext(); itemIndex++) {
+                final TemplateModel item = it.next();
+                writeObjectEntry(env, tagOut, valueProperty, labelProperty, 
objectWrapperAndUnwrapper.unwrap(item),
+                        itemIndex);
+            }
+        } else if (items instanceof TemplateHashModelEx) {
+            int itemIndex = 0;
+
+            for (KeyValuePairIterator it = ((TemplateHashModelEx) 
items).keyValuePairIterator(); it
+                    .hasNext(); itemIndex++) {
+                final KeyValuePair pair = it.next();
+                final Object key = 
objectWrapperAndUnwrapper.unwrap(pair.getKey());
+                final Object value = 
objectWrapperAndUnwrapper.unwrap(pair.getValue());
+                writeKeyValueEntry(env, tagOut, valueProperty, labelProperty, 
key, value, itemIndex);
+            }
+        } else {
+            final Object itemsObject = objectWrapperAndUnwrapper.unwrap(items);
+
+            if (itemsObject.getClass().isArray()) {
+                final Object[] itemsArray = (Object[]) itemsObject;
+
+                for (int i = 0; i < itemsArray.length; i++) {
+                    final Object item = itemsArray[i];
+                    writeObjectEntry(env, tagOut, valueProperty, 
labelProperty, item, i);
+                }
+            } else if (itemsObject instanceof Collection) {
+                final Collection<Object> optionCollection = 
(Collection<Object>) itemsObject;
+                int itemIndex = 0;
+
+                for (Object item : optionCollection) {
+                    writeObjectEntry(env, tagOut, valueProperty, 
labelProperty, item, itemIndex++);
+                }
+            } else if (itemsObject instanceof Map) {
+                final Map<Object, Object> optionMap = (Map<Object, Object>) 
itemsObject;
+                int itemIndex = 0;
+
+                for (Map.Entry<Object, Object> entry : optionMap.entrySet()) {
+                    final Object key = entry.getKey();
+                    final Object value = entry.getValue();
+                    writeKeyValueEntry(env, tagOut, valueProperty, 
labelProperty, key, value, itemIndex++);
+                }
+            } else {
+                throw new TemplateException(
+                        "The 'items' template model must be 
TemplateIterableModel or TemplateHashModelEx, "
+                                + "or represent an array, a Collection or a 
Map.");
+            }
+        }
+
+        writeAdditionalDetails(env, tagOut);
+    }
+
+    public TemplateModel getItems() {
+        return items;
+    }
+
+    public String getItemValue() {
+        return itemValue;
+    }
+
+    public String getItemLabel() {
+        return itemLabel;
+    }
+
+    public String getEnclosingElementName() {
+        return enclosingElementName;
+    }
+
+    public String getItemElementDelimiter() {
+        return itemElementDelimiter;
+    }
+
+    /**
+     * Appends a counter to the ID value because there will be multiple HTML 
elements.
+     */
+    @Override
+    protected String resolveId(Environment env) throws TemplateException {
+        Object id = evaluate(ID_PARAM_NAME, getId());
+
+        if (id != null) {
+            String idString = id.toString();
+            return (StringUtils.hasText(idString)) ? 
TagIdGenerationUtils.getNextId(env, idString) : null;
+        }
+
+        return autogenerateId(env);
+    }
+
+    protected abstract void writeAdditionalDetails(Environment env, 
TagOutputter tagOut)
+            throws TemplateException, IOException;
+
+    private void writeObjectEntry(final Environment env, final TagOutputter 
tagOut, String valueProperty,
+            String labelProperty, Object item, int itemIndex) throws 
TemplateException, IOException {
+        final BeanWrapper wrapper = 
PropertyAccessorFactory.forBeanPropertyAccess(item);
+        Object renderValue;
+
+        if (valueProperty != null) {
+            renderValue = wrapper.getPropertyValue(valueProperty);
+        } else if (item instanceof Enum) {
+            renderValue = ((Enum<?>) item).name();
+        } else {
+            renderValue = item;
+        }
+
+        final Object renderLabel = (labelProperty != null) ? 
wrapper.getPropertyValue(labelProperty) : item;
+        writeElementTag(env, tagOut, item, renderValue, renderLabel, 
itemIndex);
+    }
+
+    private void writeKeyValueEntry(final Environment env, final TagOutputter 
tagOut, String valueProperty,
+            String labelProperty, Object key, Object value, int itemIndex) 
throws TemplateException, IOException {
+        final BeanWrapper keyWrapper = 
PropertyAccessorFactory.forBeanPropertyAccess(key);
+        final BeanWrapper valueWrapper = 
PropertyAccessorFactory.forBeanPropertyAccess(value);
+        final Object renderValue = (valueProperty != null ? 
keyWrapper.getPropertyValue(valueProperty)
+                : key.toString());
+        final Object renderLabel = (labelProperty != null ? 
valueWrapper.getPropertyValue(labelProperty)
+                : value.toString());
+        writeElementTag(env, tagOut, key, renderValue, renderLabel, itemIndex);
+    }
+
+    private void writeElementTag(final Environment env, final TagOutputter 
tagOut, Object item, Object value,
+            Object label, int itemIndex) throws TemplateException, IOException 
{
+        final boolean enclosingByElem = 
StringUtils.hasText(getEnclosingElementName());
+
+        if (enclosingByElem) {
+            tagOut.beginTag(getEnclosingElementName());
+        }
+
+        if (itemIndex > 0) {
+            Object resolvedDelimiter = evaluate("delimiter", 
getItemElementDelimiter());
+
+            if (resolvedDelimiter != null) {
+                tagOut.appendValue(resolvedDelimiter.toString());
+            }
+        }
+
+        tagOut.beginTag("input");
+        String id = resolveId(env);
+        writeOptionalAttribute(tagOut, "id", id);
+        writeOptionalAttribute(tagOut, "name", getName());
+        writeOptionalAttributes(tagOut);
+        tagOut.writeAttribute("type", getInputType());
+        renderFromValue(env, item, value, tagOut);
+        tagOut.endTag();
+
+        tagOut.beginTag("label");
+        tagOut.writeAttribute("for", id);
+        tagOut.appendValue(getDisplayString(label, getBindStatus()));
+        tagOut.endTag();
+
+        if (enclosingByElem) {
+            tagOut.endTag();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/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
index 48b9498..5424ada 100644
--- 
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
@@ -48,10 +48,10 @@ abstract class 
AbstractSingleCheckedElementTemplateDirectiveModel extends Abstra
             throws TemplateException, IOException {
         super.executeInternal(args, callPlace, out, env, 
objectWrapperAndUnwrapper, requestContext);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag("input");
-        String id = resolveId();
+        String id = resolveId(env);
         writeOptionalAttribute(tagOut, "id", id);
         writeOptionalAttribute(tagOut, "name", getName());
         writeOptionalAttributes(tagOut);

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ButtonTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ButtonTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ButtonTemplateDirectiveModel.java
index 9fa9c1f..40597d6 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ButtonTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/ButtonTemplateDirectiveModel.java
@@ -115,7 +115,7 @@ class ButtonTemplateDirectiveModel extends 
AbstractHtmlElementTemplateDirectiveM
         value = CallableUtils.getOptionalStringArgument(args, VALUE_PARAM_IDX, 
this);
         disabled = CallableUtils.getOptionalBooleanArgument(args, 
DISABLED_PARAM_IDX, this, false);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag("button");
 

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModel.java
new file mode 100644
index 0000000..e846d9f
--- /dev/null
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModel.java
@@ -0,0 +1,144 @@
+/*
+ * 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 multiple HTML 
'{@code <input type="checkbox"/>}' elements.
+ * 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;#assign foodItems = [ "Sandwich", "Spaghetti", "Sushi" ] &gt;
+ *   &lt;@form.checkboxes "user.favoriteFood" items=foodItems /&gt;
+ * </PRE>
+ * Or,
+ * <PRE>
+ *   &lt;#assign foodItemHash = { "Sandwich": "Delicious sandwich", 
"Spaghetti": "Lovely spaghetti", "Sushi": "Sushi with wasabi" } &gt;
+ *   &lt;@form.checkboxes "user.favoriteFood" items=foodItemHash /&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 CheckboxesTemplateDirectiveModel extends 
AbstractMultiCheckedElementTemplateDirectiveModel {
+
+    public static final String NAME = "checkboxes";
+
+    private static final int NAMED_ARGS_OFFSET = 
AbstractMultiCheckedElementTemplateDirectiveModel.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(
+                    
AbstractMultiCheckedElementTemplateDirectiveModel.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 CheckboxesTemplateDirectiveModel(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);
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    @Override
+    protected void writeAdditionalDetails(Environment env, TagOutputter 
tagOut) throws TemplateException, IOException {
+        if (!isDisabled()) {
+            // Spring Web MVC requires to render a hidden input as a 'field 
was present' marker.
+            // Write out the '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/cb072d7d/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
index 8910214..8adac5c 100644
--- 
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
@@ -142,7 +142,7 @@ class ErrorsTemplateDirectiveModel extends 
AbstractHtmlElementTemplateDirectiveM
         delimiter = (StringUtils.hasText(param)) ? param : DEFAULT_DELIMITER;
 
         if (!callPlace.hasNestedContent()) {
-            TagOutputter tagOut = new TagOutputter(out);
+            TagOutputter tagOut = new TagOutputter(env, out);
             renderDefaultContent(tagOut);
             return;
         }
@@ -187,7 +187,7 @@ class ErrorsTemplateDirectiveModel extends 
AbstractHtmlElementTemplateDirectiveM
     }
 
     @Override
-    protected String autogenerateId() throws TemplateException {
+    protected String autogenerateId(Environment env) throws TemplateException {
         String path = getPropertyPath();
 
         if ("".equals(path) || "*".equals(path)) {

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/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 542ef03..a3f084e 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
@@ -195,7 +195,7 @@ class FormTemplateDirectiveModel extends 
AbstractHtmlElementTemplateDirectiveMod
         servletRelativeAction = CallableUtils.getOptionalStringArgument(args, 
SERVLET_RELATIVE_ACTION_PARAM_IDX, this);
         methodParam = CallableUtils.getOptionalStringArgument(args, 
METHOD_PARAM_PARAM_IDX, this);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag(FORM_TAG_NAME);
         writeDefaultAttributes(tagOut);

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/HiddenInputTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/HiddenInputTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/HiddenInputTemplateDirectiveModel.java
index ba9c192..3cbcdd3 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/HiddenInputTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/HiddenInputTemplateDirectiveModel.java
@@ -103,7 +103,7 @@ class HiddenInputTemplateDirectiveModel extends 
AbstractHtmlElementTemplateDirec
 
         disabled = CallableUtils.getOptionalBooleanArgument(args, 
DISABLED_PARAM_IDX, this, false);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag("input");
 

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/InputTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/InputTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/InputTemplateDirectiveModel.java
index f111704..e262d3f 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/InputTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/InputTemplateDirectiveModel.java
@@ -132,7 +132,7 @@ class InputTemplateDirectiveModel extends 
AbstractHtmlInputElementTemplateDirect
         onselect = CallableUtils.getOptionalStringArgument(args, 
ONSELECT_PARAM_IDX, this);
         autocomplete = CallableUtils.getOptionalStringArgument(args, 
AUTOCOMPLETE_PARAM_IDX, this);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag(NAME);
 

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/LabelTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/LabelTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/LabelTemplateDirectiveModel.java
index efb635c..b768566 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/LabelTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/LabelTemplateDirectiveModel.java
@@ -106,7 +106,7 @@ class LabelTemplateDirectiveModel extends 
AbstractHtmlElementTemplateDirectiveMo
 
         forId = CallableUtils.getOptionalStringArgument(args, 
FOR_ID_PARAM_IDX, this);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag(NAME);
         tagOut.writeAttribute(FOR_ATTRIBUTE, resolveFor());

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java
index b6f8c76..b94c1c4 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java
@@ -121,7 +121,7 @@ class OptionTemplateDirectiveModel extends 
AbstractHtmlInputElementTemplateDirec
         final TagOutputter tagOut = formTemplateScope.getCurrentTagOutputter();
 
         tagOut.beginTag("option");
-        writeOptionalAttribute(tagOut, "id", resolveId());
+        writeOptionalAttribute(tagOut, "id", resolveId(env));
         writeOptionalAttributes(tagOut);
         String renderedValue = getDisplayString(value, 
curSelectDirective.getBindStatus().getEditor());
         renderedValue = processFieldValue(env, selectName, renderedValue, 
"option");
@@ -144,7 +144,7 @@ class OptionTemplateDirectiveModel extends 
AbstractHtmlInputElementTemplateDirec
     }
 
     @Override
-    protected String autogenerateId() {
+    protected String autogenerateId(Environment env) {
         return null;
     }
 }

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/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 c405437..6fe29a6 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
@@ -130,7 +130,7 @@ class SelectTemplateDirectiveModel extends 
AbstractHtmlInputElementTemplateDirec
         size = CallableUtils.getOptionalStringArgument(args, SIZE_PARAM_IDX, 
this);
         multiple = CallableUtils.getOptionalArgument(args, MULTIPLE_PARAM_IDX, 
TemplateModel.class, this);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag(NAME);
         writeDefaultAttributes(tagOut);

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

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagIdGenerationUtils.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagIdGenerationUtils.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagIdGenerationUtils.java
new file mode 100644
index 0000000..b144063
--- /dev/null
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagIdGenerationUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+
+class TagIdGenerationUtils {
+
+    private static final String ENV_VARIABLE_NAME_PREFIX = 
TagIdGenerationUtils.class.getName() + ".";
+
+    private TagIdGenerationUtils() {
+    }
+
+    public static String getNextId(final Environment env, final String name) 
throws TemplateException {
+        final String varName = ENV_VARIABLE_NAME_PREFIX + name;
+        TemplateNumberModel curCountModel = (TemplateNumberModel) 
env.getGlobalVariable(varName);
+        int curCount;
+
+        if (curCountModel == null) {
+            curCount = 1;
+            curCountModel = new SimpleNumber(curCount);
+        } else {
+            curCount = curCountModel.getAsNumber().intValue() + 1;
+            curCountModel = new SimpleNumber(curCount);
+        }
+
+        env.setGlobalVariable(varName, curCountModel);
+
+        return name + curCount;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagOutputter.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagOutputter.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagOutputter.java
index 4e127b8..6f5b599 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagOutputter.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TagOutputter.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import java.io.Writer;
 import java.util.Stack;
 
+import org.apache.freemarker.core.Environment;
 import org.apache.freemarker.core.TemplateException;
 import org.springframework.util.StringUtils;
 
@@ -31,14 +32,21 @@ import org.springframework.util.StringUtils;
  */
 class TagOutputter {
 
+    private final Environment env;
+
     private final Writer out;
 
     private final Stack<TagEntry> tagStack = new Stack<TagEntry>();
 
-    public TagOutputter(final Writer out) {
+    public TagOutputter(final Environment env, final Writer out) {
+        this.env = env;
         this.out = out;
     }
 
+    public Environment getEnvironment() {
+        return env;
+    }
+
     public void beginTag(String tagName) throws TemplateException, IOException 
{
         if (!tagStack.isEmpty()) {
             closeAndMarkAsBlockTag();

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TextareaTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TextareaTemplateDirectiveModel.java
 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TextareaTemplateDirectiveModel.java
index 9de22f9..bfc5bde 100644
--- 
a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TextareaTemplateDirectiveModel.java
+++ 
b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/TextareaTemplateDirectiveModel.java
@@ -115,7 +115,7 @@ class TextareaTemplateDirectiveModel extends 
AbstractHtmlInputElementTemplateDir
         cols = CallableUtils.getOptionalStringArgument(args, COLS_PARAM_IDX, 
this);
         onselect = CallableUtils.getOptionalStringArgument(args, 
ONSELECT_PARAM_IDX, this);
 
-        TagOutputter tagOut = new TagOutputter(out);
+        TagOutputter tagOut = new TagOutputter(env, out);
 
         tagOut.beginTag(NAME);
 

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/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
index 5d74d8c..6aa051c 100644
--- 
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
@@ -63,8 +63,8 @@ public class CheckboxTemplateDirectiveModelTest {
         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='checkbox' 
and @id='receiveNewsletter1' and 
@name='receiveNewsletter']/@value").string("true"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='receiveNewsletter1' and 
@name='receiveNewsletter']/@checked").doesNotExist())
                 .andExpect(xpath("//form[@id='form1']//input[@type='hidden' 
and @name='_receiveNewsletter']/@value").string("on"));
     }
 
@@ -74,8 +74,8 @@ public class CheckboxTemplateDirectiveModelTest {
         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='checkbox' 
and @id='receiveNewsletter1' and 
@name='receiveNewsletter']/@value").string("true"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='receiveNewsletter1' and 
@name='receiveNewsletter']/@checked").string("checked"))
                 .andExpect(xpath("//form[@id='form1']//input[@type='hidden' 
and @name='_receiveNewsletter']/@value").string("on"));
     }
 
@@ -85,13 +85,11 @@ public class CheckboxTemplateDirectiveModelTest {
         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());
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood1' and @name='favoriteFood' and @value='Sandwich' and 
@checked='checked']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood2' and @name='favoriteFood' and @value='Spaghetti' and 
@checked='checked']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood3' and @name='favoriteFood' and @value='Sushi']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood3' and @name='favoriteFood' and 
@value='Sushi']/@checked").doesNotExist())
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' 
and @name='_favoriteFood' and @value='on']").exists());
     }
 
     @Test
@@ -100,12 +98,10 @@ public class CheckboxTemplateDirectiveModelTest {
         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"));
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood1' and @name='favoriteFood' and @value='Sandwich' and 
@checked='checked']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood2' and @name='favoriteFood' and 
@value='Spaghetti']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood3' and @name='favoriteFood' and @value='Sushi' and 
@checked='checked']").exists())
+                .andExpect(xpath("//form[@id='form1']//input[@type='checkbox' 
and @id='favoriteFood3' and @name='favoriteFood' and 
@value='Spaghetti']/@checked").doesNotExist())
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' 
and @name='_favoriteFood' and @value='on']").exists());
     }
 }

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModelTest.java
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModelTest.java
 
b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModelTest.java
new file mode 100644
index 0000000..76b125b
--- /dev/null
+++ 
b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/CheckboxesTemplateDirectiveModelTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 CheckboxesTemplateDirectiveModelTest {
+
+    @Autowired
+    private WebApplicationContext wac;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    private MockMvc mockMvc;
+
+    @Before
+    public void setUp() {
+        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
+    }
+
+    @Test
+    public void testCheckboxesWithSequence() throws Exception {
+        final User user = userRepository.getUserByEmail("j...@example.com");
+        mockMvc.perform(get("/users/{userId}/", 
user.getId()).param("viewName", "test/model/form/checkboxes-directive-usages")
+                
.accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                
.andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                
.andExpect(xpath("//form[@id='form1']//span/input[@type='checkbox' and 
@id='favoriteFoodInForm11' and @name='favoriteFood' and 
@checked='checked']/@value").string("Sandwich"))
+                
.andExpect(xpath("//form[@id='form1']//span/label[@for='favoriteFoodInForm11']").string("Sandwich"))
+                
.andExpect(xpath("//form[@id='form1']//span/input[@type='checkbox' and 
@id='favoriteFoodInForm12' and @name='favoriteFood' and 
@checked='checked']/@value").string("Spaghetti"))
+                
.andExpect(xpath("//form[@id='form1']//span/label[@for='favoriteFoodInForm12']").string("Spaghetti"))
+                
.andExpect(xpath("//form[@id='form1']//span/input[@type='checkbox' and 
@id='favoriteFoodInForm13' and @name='favoriteFood']/@value").string("Sushi"))
+                
.andExpect(xpath("//form[@id='form1']//span/label[@for='favoriteFoodInForm13']").string("Sushi"))
+                .andExpect(xpath("//form[@id='form1']//input[@type='hidden' 
and @name='_favoriteFood']/@value").string("on"));
+    }
+
+    @Test
+    public void testCheckboxesWithHash() throws Exception {
+        final User user = userRepository.getUserByEmail("j...@example.com");
+        mockMvc.perform(get("/users/{userId}/", 
user.getId()).param("viewName", "test/model/form/checkboxes-directive-usages")
+                
.accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                
.andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                
.andExpect(xpath("//form[@id='form2']//span/input[@type='checkbox' and 
@id='favoriteFoodInForm21' and @name='favoriteFood' and 
@checked='checked']/@value").string("Sandwich"))
+                
.andExpect(xpath("//form[@id='form2']//span/label[@for='favoriteFoodInForm21']").string("Delicious
 sandwich"))
+                
.andExpect(xpath("//form[@id='form2']//span/input[@type='checkbox' and 
@id='favoriteFoodInForm22' and @name='favoriteFood' and 
@checked='checked']/@value").string("Spaghetti"))
+                
.andExpect(xpath("//form[@id='form2']//span/label[@for='favoriteFoodInForm22']").string("Lovely
 spaghetti"))
+                
.andExpect(xpath("//form[@id='form2']//span/input[@type='checkbox' and 
@id='favoriteFoodInForm23' and @name='favoriteFood']/@value").string("Sushi"))
+                
.andExpect(xpath("//form[@id='form2']//span/label[@for='favoriteFoodInForm23']").string("Sushi
 with wasabi"))
+                .andExpect(xpath("//form[@id='form2']//input[@type='hidden' 
and @name='_favoriteFood']/@value").string("on"));
+    }
+
+    @Test
+    public void testCheckboxesWithEnclosingElementNameAndDelimiter() throws 
Exception {
+        final User user = userRepository.getUserByEmail("j...@example.com");
+        mockMvc.perform(get("/users/{userId}/", 
user.getId()).param("viewName", "test/model/form/checkboxes-directive-usages")
+                
.accept(MediaType.parseMediaType("text/html"))).andExpect(status().isOk())
+                
.andExpect(content().contentTypeCompatibleWith("text/html")).andDo(print())
+                
.andExpect(xpath("//form[@id='form3']//div/input[@type='checkbox' and 
@id='favoriteFoodInForm31' and @name='favoriteFood' and 
@checked='checked']/@value").string("Sandwich"))
+                
.andExpect(xpath("//form[@id='form3']//div/label[@for='favoriteFoodInForm31']").string("Sandwich"))
+                
.andExpect(xpath("//form[@id='form3']//div[1]/br").doesNotExist())
+                
.andExpect(xpath("//form[@id='form3']//div/input[@type='checkbox' and 
@id='favoriteFoodInForm32' and @name='favoriteFood' and 
@checked='checked']/@value").string("Spaghetti"))
+                
.andExpect(xpath("//form[@id='form3']//div/label[@for='favoriteFoodInForm32']").string("Spaghetti"))
+                .andExpect(xpath("//form[@id='form3']//div[2]/br").exists())
+                
.andExpect(xpath("//form[@id='form3']//div/input[@type='checkbox' and 
@id='favoriteFoodInForm33' and @name='favoriteFood']/@value").string("Sushi"))
+                
.andExpect(xpath("//form[@id='form3']//div/label[@for='favoriteFoodInForm33']").string("Sushi"))
+                .andExpect(xpath("//form[@id='form3']//div[3]/br").exists())
+                .andExpect(xpath("//form[@id='form3']//input[@type='hidden' 
and @name='_favoriteFood']/@value").string("on"));
+    }
+}

http://git-wip-us.apache.org/repos/asf/freemarker/blob/cb072d7d/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkboxes-directive-usages.f3ah
----------------------------------------------------------------------
diff --git 
a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkboxes-directive-usages.f3ah
 
b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkboxes-directive-usages.f3ah
new file mode 100644
index 0000000..b71a86e
--- /dev/null
+++ 
b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/checkboxes-directive-usages.f3ah
@@ -0,0 +1,76 @@
+<#--
+  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>Favorite Food</th>
+        <td>
+          <#assign foodItems = [ "Sandwich", "Spaghetti", "Sushi" ] >
+          <@form.checkboxes "user.favoriteFood" id="favoriteFoodInForm1" 
items=foodItems />
+        </td>
+      </tr>
+    </table>
+  </form>
+
+  <h1>Form 2</h1>
+  <hr/>
+  <form id="form2">
+    <table>
+      <tr>
+        <th>User:</th>
+        <td>
+          ${user.firstName} ${user.lastName} (${user.email})
+        </td>
+      </tr>
+      <tr>
+        <th>Favorite Food</th>
+        <td>
+          <#assign foodItems = { "Sandwich": "Delicious sandwich", 
"Spaghetti": "Lovely spaghetti", "Sushi": "Sushi with wasabi" } >
+          <@form.checkboxes "user.favoriteFood" id="favoriteFoodInForm2" 
items=foodItems />
+        </td>
+      </tr>
+    </table>
+  </form>
+
+  <h1>Form 3</h1>
+  <hr/>
+  <form id="form3">
+    <table>
+      <tr>
+        <th>Favorite Food</th>
+        <td>
+          <#assign foodItems = [ "Sandwich", "Spaghetti", "Sushi" ] >
+          <@form.checkboxes "user.favoriteFood" id="favoriteFoodInForm3" 
items=foodItems element="div" delimiter="<br/>" />
+        </td>
+      </tr>
+    </table>
+  </form>
+</body>
+</html>

Reply via email to