/**
* Copyright 2005-2010 hdiv.org
*
* 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.hdiv.web.servlet.tags.form;
import java.util.Collection;
import java.util.Map;
import javax.servlet.jsp.JspException;
import org.hdiv.dataComposer.IDataComposer;
import org.hdiv.web.util.TagUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.servlet.support.BindStatus;
import org.springframework.web.servlet.tags.form.OptionTag;
import org.springframework.web.servlet.tags.form.OptionsTag;
import org.springframework.web.servlet.tags.form.SelectTag;
import org.springframework.web.servlet.tags.form.TagWriter;
/**
* Databinding-aware JSP tag that renders an HTML '<code>select</code>'
* element.
*
* <p>Inner '<code>option</code>' tags can be rendered using one of the
* approaches supported by the OptionWriter class.
*
* <p>Also supports the use of nested {@link OptionTag OptionTags} or
* (typically one) nested {@link OptionsTag}.
*
* @author Gorka Vicente
* @since HDIV 2.0.6
* @see org.springframework.web.servlet.tags.form.SelectTag
*/
public class SelectTagHDIV extends SelectTag {
private IDataComposer dataComposer;
/**
* Marker object for items that have been specified but resolve to null.
* Allows to differentiate between 'set but null' and 'not set at all'.
*/
private static final Object EMPTY = new Object();
/**
* The {@link TagWriter} instance that the output is being written.
* <p>Only used in conjunction with nested {@link OptionTag OptionTags}.
*/
private TagWriter tagWriter;
/**
* Renders the HTML '<code>select</code>' tag to the supplied
* {@link TagWriter}.
* <p>Renders nested '<code>option</code>' tags if the
* {@link #setItems items} property is set, otherwise exposes the
* bound value for the nested {@link OptionTag OptionTags}.
*/
@Override
protected int writeTagContent(TagWriter tagWriter) throws JspException {
dataComposer = (IDataComposer) this.pageContext.getRequest().getAttribute(TagUtils.DATA_COMPOSER);
dataComposer.compose(this.getName(), "", false);
tagWriter.startTag("select");
writeDefaultAttributes(tagWriter);
if (isMultiple()) {
tagWriter.writeAttribute("multiple", "multiple");
}
tagWriter.writeOptionalAttributeValue("size", getDisplayString(evaluate("size", getSize())));
Object items = getItems();
if (items != null) {
// Items specified, but might still be empty...
if (items != EMPTY) {
Object itemsObject = evaluate("items", items);
if (itemsObject != null) {
String valueProperty = (getItemValue() != null ? ObjectUtils.getDisplayString(evaluate("itemValue", getItemValue())) : null);
String labelProperty = (getItemLabel() != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", getItemLabel())) : null);
OptionWriterHDIV optionWriter = new OptionWriterHDIV(dataComposer, this.getName(), itemsObject, getBindStatus(), valueProperty, labelProperty, isHtmlEscape());
optionWriter.writeOptions(tagWriter);
}
}
tagWriter.endTag(true);
writeHiddenTagIfNecessary(tagWriter);
return SKIP_BODY;
}
else {
// Using nested <form:option/> tags, so just expose the value in the PageContext...
tagWriter.forceBlock();
this.tagWriter = tagWriter;
this.pageContext.setAttribute(LIST_VALUE_PAGE_ATTRIBUTE, getBindStatus());
return EVAL_BODY_INCLUDE;
}
}
/**
* If using a multi-select, a hidden element is needed to make sure all
* items are correctly unselected on the server-side in response to a
* <code>null</code> post.
*/
private void writeHiddenTagIfNecessary(TagWriter tagWriter) throws JspException {
if (isMultiple()) {
String hdivValue = dataComposer.compose(WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName(), "1", false);
tagWriter.startTag("input");
tagWriter.writeAttribute("type", "hidden");
tagWriter.writeAttribute("name", WebDataBinder.DEFAULT_FIELD_MARKER_PREFIX + getName());
tagWriter.writeAttribute("value", hdivValue);
tagWriter.endTag();
}
}
private boolean isMultiple() throws JspException {
Object multiple = getMultiple();
if (Boolean.TRUE.equals(multiple) || "multiple".equals(multiple)) {
return true;
}
else if (super.getMultiple() instanceof String) {
return evaluateBoolean("multiple", (String) multiple);
}
return forceMultiple();
}
/**
* Returns '<code>true</code>' if the bound value requires the
* resultant '<code>select</code>' tag to be multi-select.
*/
private boolean forceMultiple() throws JspException {
BindStatus bindStatus = getBindStatus();
Class valueType = bindStatus.getValueType();
if (valueType != null && typeRequiresMultiple(valueType)) {
return true;
} else if (bindStatus.getEditor() != null) {
Object editorValue = bindStatus.getEditor().getValue();
if (editorValue != null && typeRequiresMultiple(editorValue.getClass())) {
return true;
}
}
return false;
}
/**
* Returns '<code>true</code>' for arrays, {@link Collection Collections}
* and {@link Map Maps}.
*/
private static boolean typeRequiresMultiple(Class type) {
return (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type));
}
/**
* Get the {@link BindStatus} for this tag.
*/
@Override
protected BindStatus getBindStatus() throws JspException {
return super.getBindStatus();
}
/**
* Get the value for the HTML '<code>name</code>' attribute.
* <p>The default implementation simply delegates to
* {@link #getCompletePath()} to use the property path as the name.
* For the most part this is desirable as it links with the server-side
* expectation for databinding. However, some subclasses may wish to change
* the value of the '<code>name</code>' attribute without changing the bind path.
* @return the value for the HTML '<code>name</code>' attribute
*/
@Override
protected String getName() throws JspException {
return super.getName();
}
/**
* Set the {@link Collection}, {@link Map} or array of objects used to
* generate the inner '<code>option</code>' tags.
* <p>Required when wishing to render '<code>option</code>' tags from
* an array, {@link Collection} or {@link Map}.
* <p>Typically a runtime expression.
* @param items the items that comprise the options of this selection
*/
@Override
public void setItems(Object items) {
super.setItems(items != null ? items : EMPTY);
}
/**
* Closes any block tag that might have been opened when using
* nested {@link OptionTag options}.
*/
@Override
public int doEndTag() throws JspException {
if (this.tagWriter != null) {
this.tagWriter.endTag();
writeHiddenTagIfNecessary(tagWriter);
}
return EVAL_PAGE;
}
/**
* Clears the {@link TagWriter} that might have been left over when using
* nested {@link OptionTag options}.
*/
@Override
public void doFinally() {
super.doFinally();
this.tagWriter = null;
this.pageContext.removeAttribute(LIST_VALUE_PAGE_ATTRIBUTE);
}
}