Repository: freemarker Updated Branches: refs/heads/3 c4f96d918 -> 2d799d492
FREEMARKER-55: Adding option directive, TODO: check option selected. Project: http://git-wip-us.apache.org/repos/asf/freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/freemarker/commit/2d799d49 Tree: http://git-wip-us.apache.org/repos/asf/freemarker/tree/2d799d49 Diff: http://git-wip-us.apache.org/repos/asf/freemarker/diff/2d799d49 Branch: refs/heads/3 Commit: 2d799d49203617efdd2a3de6fa916f6c67014d05 Parents: c4f96d9 Author: Woonsan Ko <[email protected]> Authored: Fri Apr 27 00:21:33 2018 -0400 Committer: Woonsan Ko <[email protected]> Committed: Fri Apr 27 00:21:33 2018 -0400 ---------------------------------------------------------------------- .../form/OptionTemplateDirectiveModel.java | 154 +++++++++++++++++ .../form/SelectableValueComparisonUtils.java | 165 +++++++++++++++++++ .../SpringFormTemplateCallableHashModel.java | 1 + .../form/SelectTemplateDirectiveModelTest.java | 6 + .../model/form/select-directive-usages.f3ah | 16 ++ 5 files changed, 342 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/freemarker/blob/2d799d49/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 new file mode 100644 index 0000000..7ad31ad --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/OptionTemplateDirectiveModel.java @@ -0,0 +1,154 @@ +/* + * 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.util.ObjectUtils; +import org.springframework.web.servlet.support.RequestContext; + +/** + * Provides a convenient <code>TemplateModel</code> that allow to supply a collection that are to be rendered + * as an HTML '{@code option}' element. + * <P> + * This directive supports the following parameters: + * <UL> + * <LI><code>items</code>: collection of option items.</LI> + * <LI> + * ... TODO ... + * </LI> + * </UL> + * </P> + * <P> + * Some valid example(s): + * </P> + * <PRE> + * ... + * </PRE> + * <P> + * <EM>Note:</EM> Unlike Spring Framework's <code><form:input /></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 OptionTemplateDirectiveModel extends AbstractHtmlInputElementTemplateDirectiveModel { + + public static final String NAME = "option"; + + private static final int NAMED_ARGS_OFFSET = AbstractHtmlInputElementTemplateDirectiveModel.ARGS_LAYOUT + .getPredefinedNamedArgumentsEndIndex(); + + private static final int VALUE_PARAM_IDX = NAMED_ARGS_OFFSET; + private static final String VALUE_PARAM_NAME = "value"; + + private static final int LABEL_PARAM_IDX = NAMED_ARGS_OFFSET + 1; + private static final String LABEL_PARAM_NAME = "label"; + + protected static final ArgumentArrayLayout ARGS_LAYOUT = ArgumentArrayLayout.create(1, false, + StringToIndexMap.of( + AbstractHtmlInputElementTemplateDirectiveModel.ARGS_LAYOUT.getPredefinedNamedArgumentsMap(), + new StringToIndexMap.Entry(VALUE_PARAM_NAME, VALUE_PARAM_IDX), + new StringToIndexMap.Entry(LABEL_PARAM_NAME, LABEL_PARAM_IDX)), + true); + + private String value; + private String label; + + protected OptionTemplateDirectiveModel(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + @Override + public boolean isNestedContentSupported() { + return false; + } + + @Override + public ArgumentArrayLayout getDirectiveArgumentArrayLayout() { + return ARGS_LAYOUT; + } + + @Override + protected void executeInternal(TemplateModel[] args, CallPlace callPlace, Writer out, final Environment env, + ObjectWrapperAndUnwrapper objectWrapperAndUnwrapper, RequestContext requestContext) + throws TemplateException, IOException { + + super.executeInternal(args, callPlace, out, env, objectWrapperAndUnwrapper, requestContext); + + value = CallableUtils.getOptionalStringArgument(args, VALUE_PARAM_IDX, this); + label = CallableUtils.getOptionalStringArgument(args, LABEL_PARAM_IDX, this); + + final FormTemplateScope formTemplateScope = env.getCustomState(FORM_TEMPLATE_SCOPE_KEY); + final SelectTemplateDirectiveModel curSelectDirective = formTemplateScope.getCurrentSelectDirective(); + + final String selectName = curSelectDirective.getName(); + final String valueProperty = (getValue() != null + ? ObjectUtils.getDisplayString(evaluate("value", getValue())) + : null); + final String labelProperty = (getLabel() != null + ? ObjectUtils.getDisplayString(evaluate("label", getLabel())) + : null); + + final TagOutputter tagOut = formTemplateScope.getCurrentTagOutputter(); + + tagOut.beginTag("option"); + writeOptionalAttribute(tagOut, "id", resolveId()); + writeOptionalAttributes(tagOut); + String renderedValue = getDisplayString(value, curSelectDirective.getBindStatus().getEditor()); + renderedValue = processFieldValue(env, selectName, renderedValue, "option"); + tagOut.writeAttribute("value", renderedValue); + + if (SelectableValueComparisonUtils.isEqualValueBoundTo(value, curSelectDirective.getBindStatus())) { + tagOut.writeAttribute("selected", "selected"); + } + + if (isDisabled()) { + tagOut.writeAttribute("disabled", "disabled"); + } + + tagOut.appendValue(label); + tagOut.endTag(); + } + + public String getValue() { + return value; + } + + public String getLabel() { + return label; + } + + @Override + protected String autogenerateId() { + return null; + } +} http://git-wip-us.apache.org/repos/asf/freemarker/blob/2d799d49/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java new file mode 100644 index 0000000..e17ab05 --- /dev/null +++ b/freemarker-spring/src/main/java/org/apache/freemarker/spring/model/form/SelectableValueComparisonUtils.java @@ -0,0 +1,165 @@ +/* + * 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.beans.PropertyEditor; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.web.servlet.support.BindStatus; + +class SelectableValueComparisonUtils { + + private SelectableValueComparisonUtils() { + } + + public static boolean isEqualValueBoundTo(final Object value, final BindStatus bindStatus) { + if (value == null && bindStatus == null) { + return true; + } + + Object boundValue = bindStatus.getValue(); + + if (ObjectUtils.nullSafeEquals(boundValue, value)) { + return true; + } + + final Object actualValue = bindStatus.getActualValue(); + + if (actualValue != null && actualValue != boundValue && ObjectUtils.nullSafeEquals(actualValue, value)) { + return true; + } + + if (actualValue != null) { + boundValue = actualValue; + } else if (boundValue == null) { + return false; + } + + boolean equal = false; + + if (boundValue.getClass().isArray()) { + equal = isValueInCollection(CollectionUtils.arrayToList(boundValue), value, bindStatus); + } else if (boundValue instanceof Collection) { + equal = isValueInCollection((Collection<?>) boundValue, value, bindStatus); + } else if (boundValue instanceof Map) { + equal = isValueInMapKeys((Map<?, ?>) boundValue, value, bindStatus); + } + + if (!equal) { + equal = isEqualValuesComparingWithEditorValue(boundValue, value, bindStatus.getEditor(), null); + } + + return equal; + } + + private static boolean isValueInCollection(Collection<?> boundCollection, Object value, + BindStatus bindStatus) { + try { + if (boundCollection.contains(value)) { + return true; + } + } catch (ClassCastException ignoreEx) { + } + + return isValueInCollectionComparingWithEditorValue(boundCollection, value, bindStatus); + } + + private static boolean isValueInMapKeys(Map<?, ?> boundMap, Object value, BindStatus bindStatus) { + try { + if (boundMap.containsKey(value)) { + return true; + } + } catch (ClassCastException ignoreEx) { + } + + return isValueInCollectionComparingWithEditorValue(boundMap.keySet(), value, bindStatus); + } + + private static boolean isValueInCollectionComparingWithEditorValue(Collection<?> collection, Object value, + BindStatus bindStatus) { + + final Map<PropertyEditor, Object> convertedValueCache = new HashMap<PropertyEditor, Object>(1); + PropertyEditor editor = null; + final boolean isValueString = (value instanceof String); + + if (!isValueString) { + editor = bindStatus.findEditor(value.getClass()); + } + + for (Object element : collection) { + if (editor == null && element != null && isValueString) { + editor = bindStatus.findEditor(element.getClass()); + } + + if (isEqualValuesComparingWithEditorValue(element, value, editor, convertedValueCache)) { + return true; + } + } + + return false; + } + + private static boolean isEqualValuesComparingWithEditorValue(Object boundValue, Object value, PropertyEditor editor, + Map<PropertyEditor, Object> convertedValueCache) { + + String valueDisplayString = AbstractFormTemplateDirectiveModel.getDisplayString(value, editor); + + if (boundValue != null && boundValue.getClass().isEnum()) { + Enum<?> boundEnum = (Enum<?>) boundValue; + String enumCodeAsString = ObjectUtils.getDisplayString(boundEnum.name()); + + if (enumCodeAsString.equals(valueDisplayString)) { + return true; + } + + String enumLabelAsString = ObjectUtils.getDisplayString(boundEnum.toString()); + + if (enumLabelAsString.equals(valueDisplayString)) { + return true; + } + } else if (ObjectUtils.getDisplayString(boundValue).equals(valueDisplayString)) { + return true; + } else if (editor != null && value instanceof String) { + String valueAsString = (String) value; + Object valueAsEditorValue; + + if (convertedValueCache != null && convertedValueCache.containsKey(editor)) { + valueAsEditorValue = convertedValueCache.get(editor); + } else { + editor.setAsText(valueAsString); + valueAsEditorValue = editor.getValue(); + + if (convertedValueCache != null) { + convertedValueCache.put(editor, valueAsEditorValue); + } + } + + if (ObjectUtils.nullSafeEquals(boundValue, valueAsEditorValue)) { + return true; + } + } + + return false; + } +} http://git-wip-us.apache.org/repos/asf/freemarker/blob/2d799d49/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 31e1591..b460c4c 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 @@ -54,6 +54,7 @@ public final class SpringFormTemplateCallableHashModel implements TemplateHashMo modelsMap.put(LabelTemplateDirectiveModel.NAME, new LabelTemplateDirectiveModel(request, response)); modelsMap.put(SelectTemplateDirectiveModel.NAME, new SelectTemplateDirectiveModel(request, response)); modelsMap.put(OptionsTemplateDirectiveModel.NAME, new OptionsTemplateDirectiveModel(request, response)); + modelsMap.put(OptionTemplateDirectiveModel.NAME, new OptionTemplateDirectiveModel(request, response)); modelsMap.put(ErrorsTemplateDirectiveModel.NAME, new ErrorsTemplateDirectiveModel(request, response)); } http://git-wip-us.apache.org/repos/asf/freemarker/blob/2d799d49/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java index c07fb8d..eafd098 100644 --- a/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java +++ b/freemarker-spring/src/test/java/org/apache/freemarker/spring/model/form/SelectTemplateDirectiveModelTest.java @@ -82,5 +82,11 @@ public class SelectTemplateDirectiveModelTest { String sport = UserController.ALL_SPORTS.get(i); resultAcctions.andExpect(xpath("//form[@id='form3']//select[@name='favoriteSport']//option[" + (i + 1) + "]").string(sport)); } + + resultAcctions.andExpect(xpath("//form[@id='form4']//select[@name='favoriteSport']//option[1]").string("--- Select ---")); + for (int i = 0; i < UserController.OUTDOOR_SPORTS.size(); i++) { + String sport = UserController.OUTDOOR_SPORTS.get(i); + resultAcctions.andExpect(xpath("//form[@id='form4']//select[@name='favoriteSport']//option[" + (i + 2) + "]").string(sport)); + } } } http://git-wip-us.apache.org/repos/asf/freemarker/blob/2d799d49/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah ---------------------------------------------------------------------- diff --git a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah index 0794c2c..327bf4a 100644 --- a/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah +++ b/freemarker-spring/src/test/resources/META-INF/web-resources/views/test/model/form/select-directive-usages.f3ah @@ -62,5 +62,21 @@ </table> </form> + <h1>Form 4</h1> + <hr/> + <form id="form4"> + <table> + <tr> + <th>Favorite Sport:</th> + <td> + <@form.select 'user.favoriteSport'> + <@form.option value="" label="--- Select ---" /> + <@form.options items=outdoorSports /> + </@form.select> + </td> + </tr> + </table> + </form> + </body> </html>
