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> + * <#assign foodItems = [ "Sandwich", "Spaghetti", "Sushi" ] > + * <@form.checkboxes "user.favoriteFood" items=foodItems /> + * </PRE> + * Or, + * <PRE> + * <#assign foodItemHash = { "Sandwich": "Delicious sandwich", "Spaghetti": "Lovely spaghetti", "Sushi": "Sushi with wasabi" } > + * <@form.checkboxes "user.favoriteFood" items=foodItemHash /> + * </PRE> + * <P> + * <EM>Note:</EM> Unlike Spring Framework's <code><form:button /></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>