/*
 * Copyright 2002-2012 the original author or authors.
 *
 * Licensed 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.springframework.web.servlet.tags.form;

import java.beans.PropertyEditor;
import java.util.Collection;
import java.util.Map;
import javax.servlet.jsp.JspException;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.support.BindStatus;

Provides supporting functionality to render a list of 'option' tags based on some source object. This object can be either an array, a Collection, or a Map.

Using an array or a Collection:

If you supply an array or Collection source object to render the inner 'option' tags, you may optionally specify the name of the property on the objects which corresponds to the value of the rendered 'option' (i.e., the valueProperty) and the name of the property that corresponds to the label (i.e., the labelProperty). These properties are then used when rendering each element of the array/Collection as an 'option'. If either property name is omitted, the value of Object.toString() of the corresponding array/Collection element is used instead. However, if the item is an enum, Enum.name() is used as the default value.

Using a Map:

You can alternatively choose to render 'option' tags by supplying a Map as the source object.

If you omit property names for the value and label:

  • the key of each Map entry will correspond to the value of the rendered 'option', and
  • the value of each Map entry will correspond to the label of the rendered 'option'.

If you supply property names for the value and label:

  • the value of the rendered 'option' will be retrieved from the valueProperty on the object corresponding to the key of each Map entry, and
  • the label of the rendered 'option' will be retrieved from the labelProperty on the object corresponding to the value of each Map entry.

When using either of these approaches:

  • Property names for the value and label are specified as arguments to the constructor.
  • An 'option' is marked as 'selected' if its key matches the value that is bound to the tag instance.
Author:Rob Harrop, Juergen Hoeller, Sam Brannen, Scott Andrews
Since:2.0
/** * Provides supporting functionality to render a list of '{@code option}' * tags based on some source object. This object can be either an array, a * {@link Collection}, or a {@link Map}. * <h3>Using an array or a {@link Collection}:</h3> * <p> * If you supply an array or {@link Collection} source object to render the * inner '{@code option}' tags, you may optionally specify the name of * the property on the objects which corresponds to the <em>value</em> of the * rendered '{@code option}' (i.e., the {@code valueProperty}) * and the name of the property that corresponds to the <em>label</em> (i.e., * the {@code labelProperty}). These properties are then used when * rendering each element of the array/{@link Collection} as an '{@code option}'. * If either property name is omitted, the value of {@link Object#toString()} of * the corresponding array/{@link Collection} element is used instead. However, * if the item is an enum, {@link Enum#name()} is used as the default value. * </p> * <h3>Using a {@link Map}:</h3> * <p> * You can alternatively choose to render '{@code option}' tags by * supplying a {@link Map} as the source object. * </p> * <p> * If you <strong>omit</strong> property names for the <em>value</em> and * <em>label</em>: * </p> * <ul> * <li>the {@code key} of each {@link Map} entry will correspond to the * <em>value</em> of the rendered '{@code option}', and</li> * <li>the {@code value} of each {@link Map} entry will correspond to * the <em>label</em> of the rendered '{@code option}'.</li> * </ul> * <p> * If you <strong>supply</strong> property names for the <em>value</em> and * <em>label</em>: * </p> * <ul> * <li>the <em>value</em> of the rendered '{@code option}' will be * retrieved from the {@code valueProperty} on the object * corresponding to the {@code key} of each {@link Map} entry, and</li> * <li>the <em>label</em> of the rendered '{@code option}' will be * retrieved from the {@code labelProperty} on the object * corresponding to the {@code value} of each {@link Map} entry. * </ul> * <h3>When using either of these approaches:</h3> * <ul> * <li>Property names for the <em>value</em> and <em>label</em> are * specified as arguments to the * {@link #OptionWriter(Object, BindStatus, String, String, boolean) constructor}.</li> * <li>An '{@code option}' is marked as 'selected' if its key * {@link #isOptionSelected matches} the value that is bound to the tag instance.</li> * </ul> * * @author Rob Harrop * @author Juergen Hoeller * @author Sam Brannen * @author Scott Andrews * @since 2.0 */
class OptionWriter { private final Object optionSource; private final BindStatus bindStatus; @Nullable private final String valueProperty; @Nullable private final String labelProperty; private final boolean htmlEscape;
Create a new OptionWriter for the supplied objectSource.
Params:
  • optionSource – the source of the options (never null)
  • bindStatus – the BindStatus for the bound value (never null)
  • valueProperty – the name of the property used to render option values (optional)
  • labelProperty – the name of the property used to render option labels (optional)
/** * Create a new {@code OptionWriter} for the supplied {@code objectSource}. * @param optionSource the source of the {@code options} (never {@code null}) * @param bindStatus the {@link BindStatus} for the bound value (never {@code null}) * @param valueProperty the name of the property used to render {@code option} values * (optional) * @param labelProperty the name of the property used to render {@code option} labels * (optional) */
public OptionWriter(Object optionSource, BindStatus bindStatus, @Nullable String valueProperty, @Nullable String labelProperty, boolean htmlEscape) { Assert.notNull(optionSource, "'optionSource' must not be null"); Assert.notNull(bindStatus, "'bindStatus' must not be null"); this.optionSource = optionSource; this.bindStatus = bindStatus; this.valueProperty = valueProperty; this.labelProperty = labelProperty; this.htmlEscape = htmlEscape; }
Write the 'option' tags for the configured optionSource to the supplied TagWriter.
/** * Write the '{@code option}' tags for the configured {@link #optionSource} to * the supplied {@link TagWriter}. */
public void writeOptions(TagWriter tagWriter) throws JspException { if (this.optionSource.getClass().isArray()) { renderFromArray(tagWriter); } else if (this.optionSource instanceof Collection) { renderFromCollection(tagWriter); } else if (this.optionSource instanceof Map) { renderFromMap(tagWriter); } else if (this.optionSource instanceof Class && ((Class<?>) this.optionSource).isEnum()) { renderFromEnum(tagWriter); } else { throw new JspException( "Type [" + this.optionSource.getClass().getName() + "] is not valid for option items"); } }
Render the inner 'option' tags using the optionSource.
See Also:
/** * Render the inner '{@code option}' tags using the {@link #optionSource}. * @see #doRenderFromCollection(java.util.Collection, TagWriter) */
private void renderFromArray(TagWriter tagWriter) throws JspException { doRenderFromCollection(CollectionUtils.arrayToList(this.optionSource), tagWriter); }
Render the inner 'option' tags using the supplied Map as the source.
See Also:
/** * Render the inner '{@code option}' tags using the supplied * {@link Map} as the source. * @see #renderOption(TagWriter, Object, Object, Object) */
private void renderFromMap(TagWriter tagWriter) throws JspException { Map<?, ?> optionMap = (Map<?, ?>) this.optionSource; for (Map.Entry<?, ?> entry : optionMap.entrySet()) { Object mapKey = entry.getKey(); Object mapValue = entry.getValue(); Object renderValue = (this.valueProperty != null ? PropertyAccessorFactory.forBeanPropertyAccess(mapKey).getPropertyValue(this.valueProperty) : mapKey); Object renderLabel = (this.labelProperty != null ? PropertyAccessorFactory.forBeanPropertyAccess(mapValue).getPropertyValue(this.labelProperty) : mapValue); renderOption(tagWriter, mapKey, renderValue, renderLabel); } }
Render the inner 'option' tags using the optionSource.
See Also:
/** * Render the inner '{@code option}' tags using the {@link #optionSource}. * @see #doRenderFromCollection(java.util.Collection, TagWriter) */
private void renderFromCollection(TagWriter tagWriter) throws JspException { doRenderFromCollection((Collection<?>) this.optionSource, tagWriter); }
Render the inner 'option' tags using the optionSource.
See Also:
/** * Render the inner '{@code option}' tags using the {@link #optionSource}. * @see #doRenderFromCollection(java.util.Collection, TagWriter) */
private void renderFromEnum(TagWriter tagWriter) throws JspException { doRenderFromCollection(CollectionUtils.arrayToList(((Class<?>) this.optionSource).getEnumConstants()), tagWriter); }
Render the inner 'option' tags using the supplied Collection of objects as the source. The value of the valueProperty field is used when rendering the 'value' of the 'option' and the value of the labelProperty property is used when rendering the label.
/** * Render the inner '{@code option}' tags using the supplied {@link Collection} of * objects as the source. The value of the {@link #valueProperty} field is used * when rendering the '{@code value}' of the '{@code option}' and the value of the * {@link #labelProperty} property is used when rendering the label. */
private void doRenderFromCollection(Collection<?> optionCollection, TagWriter tagWriter) throws JspException { for (Object item : optionCollection) { BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item); Object value; if (this.valueProperty != null) { value = wrapper.getPropertyValue(this.valueProperty); } else if (item instanceof Enum) { value = ((Enum<?>) item).name(); } else { value = item; } Object label = (this.labelProperty != null ? wrapper.getPropertyValue(this.labelProperty) : item); renderOption(tagWriter, item, value, label); } }
Render an HTML 'option' with the supplied value and label. Marks the value as 'selected' if either the item itself or its value match the bound value.
/** * Render an HTML '{@code option}' with the supplied value and label. Marks the * value as 'selected' if either the item itself or its value match the bound value. */
private void renderOption(TagWriter tagWriter, Object item, @Nullable Object value, @Nullable Object label) throws JspException { tagWriter.startTag("option"); writeCommonAttributes(tagWriter); String valueDisplayString = getDisplayString(value); String labelDisplayString = getDisplayString(label); valueDisplayString = processOptionValue(valueDisplayString); // allows render values to handle some strange browser compat issues. tagWriter.writeAttribute("value", valueDisplayString); if (isOptionSelected(value) || (value != item && isOptionSelected(item))) { tagWriter.writeAttribute("selected", "selected"); } if (isOptionDisabled()) { tagWriter.writeAttribute("disabled", "disabled"); } tagWriter.appendValue(labelDisplayString); tagWriter.endTag(); }
Determine the display value of the supplied Object, HTML-escaped as required.
/** * Determine the display value of the supplied {@code Object}, * HTML-escaped as required. */
private String getDisplayString(@Nullable Object value) { PropertyEditor editor = (value != null ? this.bindStatus.findEditor(value.getClass()) : null); return ValueFormatter.getDisplayString(value, editor, this.htmlEscape); }
Process the option value before it is written.

The default implementation simply returns the same value unchanged.

/** * Process the option value before it is written. * <p>The default implementation simply returns the same value unchanged. */
protected String processOptionValue(String resolvedValue) { return resolvedValue; }
Determine whether the supplied values matched the selected value.

Delegates to SelectedValueComparator.isSelected.

/** * Determine whether the supplied values matched the selected value. * <p>Delegates to {@link SelectedValueComparator#isSelected}. */
private boolean isOptionSelected(@Nullable Object resolvedValue) { return SelectedValueComparator.isSelected(this.bindStatus, resolvedValue); }
Determine whether the option fields should be disabled.
/** * Determine whether the option fields should be disabled. */
protected boolean isOptionDisabled() throws JspException { return false; }
Write default attributes configured to the supplied TagWriter.
/** * Write default attributes configured to the supplied {@link TagWriter}. */
protected void writeCommonAttributes(TagWriter tagWriter) throws JspException { } }