Open Source Repository

Home /spring/spring-web-servlet-3.0.5 | Repository Home



org/springframework/web/servlet/tags/form/OptionWriter.java
/*
 * Copyright 2002-2010 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.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.support.BindStatus;

/**
 * Provides supporting functionality to render a list of '<code>option</code>'
 * 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</code>' 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</code>' (i.e., the <code>valueProperty</code>)
 * and the name of the property that corresponds to the <em>label</em> (i.e.,
 * the <code>labelProperty</code>). These properties are then used when
 * rendering each element of the array/{@link Collection} as an '<code>option</code>'.
 * 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</code>' 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</code> of each {@link Map} entry will correspond to the
 <em>value</em> of the rendered '<code>option</code>', and</li>
 <li>the <code>value</code> of each {@link Map} entry will correspond to
 * the <em>label</em> of the rendered '<code>option</code>'.</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</code>' will be
 * retrieved from the <code>valueProperty</code> on the object
 * corresponding to the <code>key</code> of each {@link Map} entry, and</li>
 <li>the <em>label</em> of the rendered '<code>option</code>' will be
 * retrieved from the <code>labelProperty</code> on the object
 * corresponding to the <code>value</code> 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</code>' 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;

  private final String valueProperty;

  private final String labelProperty;

  private final boolean htmlEscape;


  /**
   * Creates a new <code>OptionWriter</code> for the supplied <code>objectSource</code>.
   @param optionSource the source of the <code>options</code> (never <code>null</code>)
   @param bindStatus the {@link BindStatus} for the bound value (never <code>null</code>)
   @param valueProperty the name of the property used to render <code>option</code> values
   * (optional)
   @param labelProperty the name of the property used to render <code>option</code> labels
   * (optional)
   */
  public OptionWriter(
      Object optionSource, BindStatus bindStatus, String valueProperty, 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 '<code>option</code>' tags for the configured {@link #optionSource} to
   * the supplied {@link TagWriter}.
   */
  public void writeOptions(TagWriter tagWriterthrows 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 && ((Classthis.optionSource).isEnum()) {
      renderFromEnum(tagWriter);
    }
    else {
      throw new JspException(
          "Type [" this.optionSource.getClass().getName() "] is not valid for option items");
    }
  }

  /**
   * Renders the inner '<code>option</code>' tags using the {@link #optionSource}.
   @see #doRenderFromCollection(java.util.Collection, TagWriter)
   */
  private void renderFromArray(TagWriter tagWriterthrows JspException {
    doRenderFromCollection(CollectionUtils.arrayToList(this.optionSource), tagWriter);
  }

  /**
   * Renders the inner '<code>option</code>' tags using the supplied
   {@link Map} as the source.
   @see #renderOption(TagWriter, Object, Object, Object)
   */
  private void renderFromMap(TagWriter tagWriterthrows JspException {
    Map<?, ?> optionMap = (Mapthis.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);
    }
  }

  /**
   * Renders the inner '<code>option</code>' tags using the {@link #optionSource}.
   @see #doRenderFromCollection(java.util.Collection, TagWriter)
   */
  private void renderFromCollection(TagWriter tagWriterthrows JspException {
    doRenderFromCollection((Collectionthis.optionSource, tagWriter);
  }

  /**
   * Renders the inner '<code>option</code>' tags using the {@link #optionSource}.
   @see #doRenderFromCollection(java.util.Collection, TagWriter)
   */
  private void renderFromEnum(TagWriter tagWriterthrows JspException {
    doRenderFromCollection(CollectionUtils.arrayToList(((Classthis.optionSource).getEnumConstants()), tagWriter);
  }

  /**
   * Renders the inner '<code>option</code>' 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</code>' of the '<code>option</code>' and the value of the
   {@link #labelProperty} property is used when rendering the label.
   */
  private void doRenderFromCollection(Collection optionCollection, TagWriter tagWriterthrows 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);
    }
  }

  /**
   * Renders an HTML '<code>option</code>' 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, Object value, Object labelthrows JspException {
    tagWriter.startTag("option");
    writeCommonAttributes(tagWriter);

    String valueDisplayString = getDisplayString(value);
    String labelDisplayString = getDisplayString(label);

    // 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();
  }

  /**
   * Determines the display value of the supplied <code>Object</code>,
   * HTML-escaped as required.
   */
  private String getDisplayString(Object value) {
    PropertyEditor editor = (value != null this.bindStatus.findEditor(value.getClass()) null);
    return ValueFormatter.getDisplayString(value, editor, this.htmlEscape);
  }

  /**
   * Determine whether the supplied values matched the selected value.
   * Delegates to {@link SelectedValueComparator#isSelected}.
   */
  private boolean isOptionSelected(Object resolvedValue) {
    return SelectedValueComparator.isSelected(this.bindStatus, resolvedValue);
  }

  /**
   * Determine whether the option fields should be disabled.
   */
  protected boolean isOptionDisabled() throws JspException {
    return false;
  }

  /**
   * Writes default attributes configured to the supplied {@link TagWriter}.
   */
  protected void writeCommonAttributes(TagWriter tagWriterthrows JspException {
  }

}