/* Copyright 2005-2006 Tim Fennell
*
* 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 net.sourceforge.stripes.tag;
import net.sourceforge.stripes.exception.StripesJspException;
import net.sourceforge.stripes.localization.LocalizationUtility;
import net.sourceforge.stripes.util.bean.BeanUtil;
import net.sourceforge.stripes.util.bean.ExpressionException;
import net.sourceforge.stripes.util.bean.BeanComparator;
import net.sourceforge.stripes.util.StringUtil;
import net.sourceforge.stripes.util.CollectionUtil;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import java.util.Collection;
import java.util.Locale;
import java.util.Collections;
import java.util.List;
import java.util.LinkedList;
/**
* <p>Writes a set of {@literal <option value="foo">bar</option>} tags to the page based on the
* contents of a Collection. Each element in the collection is represented by a single option
* tag on the page. Uses the label and value attributes on the tag to name the properties of the
* objects in the Collection that should be used to generate the body of the HTML option tag and
* the value attribute of the HTML option tag respectively.</p>
*
* <p>E.g. a tag declaration that looks like:</p>
* <pre>{@literal <stripes:options-collection collection="${cats} value="catId" label="name"/>}</pre>
*
* <p>would cause the container to look for a Collection called "cats" across the various JSP
* scopes and set it on the tag. The tag would then proceed to iterate through that collection
* calling getCatId() and getName() on each cat to produce HTML option tags.</p>
*
* <p>By default, the tag will attempt to localize the labels attributes of the option tags that are
* generated. To override this default and turn off this behavior, thus saving unnecessary resource
* bundle lookups, set the localizeLabels attribute to false.</p>
*
* <p>To do label localization, the tag will look up labels in the field resource bundle using:</p>
*
* <ul>
* <li>{className}.{labelPropertyValue}</li>
* <li>{packageName}.{className}.{labelPropertyValue}</li>
* <li>{className}.{valuePropertyValue}</li>
* <li>{packageName}.{className}.{valuePropertyValue}</li>
* </ul>
*
* <p>For example for a class com.myco.Gender supplied to the options-collection tag with
* label="description" and value="key", when rendering for an instance
* Gender[key="M", description="Male"] the following localized properties will be looked for:
*
* <ul>
* <li>Gender.Male</li>
* <li>com.myco.Gender.Male</li>
* <li>Gender.M</li>
* <li>com.myco.Gender.M</li>
* </ul>
*
* <p>If no localized label can be found, or if the localizeLabels attribute is set to false,
* then the value of the label property will be used.</p>
*
* <p>Optionally, the group attribute may be used to generate <optgroup> tags. The value of
* this attribute is used to retrieve the corresponding property on each object of the collection.
* A new optgroup will be created each time the value changes.
* </p>
*
* <p>The rendered group may be localized by specifying one of the following properties:</p>
*
* <ul>
* <li>{className}.{groupPropertyValue}</li>
* <li>{packageName}.{className}.{groupPropertyValue}</li>
* </ul>
*
* <p>All other attributes on the tag (other than collection, value, label and group) are passed directly
* through to the InputOptionTag which is used to generate the individual HTML options tags. As a
* result the InputOptionsCollectionTag will exhibit the same re-population/selection behaviour
* as the regular options tag.</p>
*
* <p>Since the tag has no use for one it does not allow a body.</p>
*
* @author Tim Fennell
*/
public class InputOptionsCollectionTag extends HtmlTagSupport {
/** A helper for writing HTML <optgroup> tags. */
private final HtmlTagSupport optgroupSupport = new HtmlTagSupport() {
@Override
public int doStartTag() throws JspException {
return 0;
}
@Override
public int doEndTag() throws JspException {
return 0;
}
};
private Collection<? extends Object> collection;
private String value;
private String label;
private String sort;
private String group;
private Boolean localizeLabels;
/**
* A little container class that holds an entry in the collection of items being used
* to generate the options, along with the determined label and value (either from a
* property, or a localized value).
*/
public static class Entry {
public Object bean, label, value, group;
Entry(Object bean, Object label, Object value, Object group) {
this.bean = bean;
this.label = label;
this.value = value;
this.group = group;
}
}
/** Internal list of entries that is assembled from the items in the collection. */
private List<Entry> entries = new LinkedList<Entry>();
/**
* <p>Sets the collection that will be used to generate options. In this case the term
* collection is used in the loosest possible sense - it means either a bonafide instance
* of {@link java.util.Collection}, or an implementation of {@link Iterable} other than a
* Collection, or an array of Objects or primitives.</p>
*
* <p>In the case of any input which is not an {@link java.util.Collection} it is converted
* to a Collection before storing it.</p>
*
* @param in either a Collection, an Iterable or an Array
*/
@SuppressWarnings("unchecked")
public void setCollection(Object in) {
if (in == null) this.collection = null;
else if (in instanceof Collection) this.collection = (Collection) in;
else if (in instanceof Iterable) this.collection = CollectionUtil.asList((Iterable) in);
else if (in.getClass().isArray()) this.collection = CollectionUtil.asList(in);
else {
throw new IllegalArgumentException
("A 'collection' was supplied that is not of a supported type: " + in.getClass());
}
}
/**
* Returns the value set by {@link #setCollection(Object)}. In the case that a
* {@link java.util.Collection} was supplied, the same collection will be returned. In all
* other cases a new collection created to hold the supplied elements will be returned.
*/
public Object getCollection() {
return this.collection;
}
/**
* Sets the name of the property that will be fetched on each bean in the collection in
* order to generate the value attribute of each option.
*
* @param value the name of the attribute
*/
public void setValue(String value) {
this.value = value;
}
/** Returns the property name set with setValue(). */
public String getValue() {
return value;
}
/**
* Sets the name of the property that will be fetched on each bean in the collection in
* order to generate the body of each option (i.e. what is seen by the user).
*
* @param label the name of the attribute
*/
public void setLabel(String label) {
this.label = label;
}
/** Gets the property name set with setLabel(). */
public String getLabel() {
return label;
}
/**
* Sets a comma separated list of properties by which the beans in the collection will
* be sorted prior to rendering them as options. 'label' and 'value' are special case
* properties that are used to indicate the generated label and value of the option.
*
* @param sort the name of the attribute(s) used to sort the collection of options
*/
public void setSort(String sort) {
this.sort = sort;
}
/** Gets the comma separated list of properties by which the collection is sorted. */
public String getSort() {
return sort;
}
/** Sets the flag that indicates whether or not attempts to localize labels should be made. */
public void setLocalizeLabels(Boolean localizeLabels) {
this.localizeLabels = localizeLabels;
}
/** Gets the flag that indicates whether or not attempts to localize labels should be made. */
public Boolean getLocalizeLabels() {
return localizeLabels;
}
protected boolean isAttemptToLocalizeLabels() {
return (localizeLabels == null) || (localizeLabels != null && localizeLabels.booleanValue());
}
/**
* Adds an entry to the internal list of items being used to generate options.
* @param item the object represented by the option
* @param label the actual label for the option
* @param value the actual value for the option
*/
protected void addEntry(Object item, Object label, Object value) {
this.entries.add(new Entry(item, label, value, null));
}
/**
* Adds an entry to the internal list of items being used to generate options.
* @param item the object represented by the option
* @param label the actual label for the option
* @param value the actual value for the option
* @param group the value to be used for optgroups
*/
protected void addEntry(Object item, Object label, Object value, Object group) {
this.entries.add(new Entry(item, label, value, group));
}
/**
* Iterates through the collection and generates the list of Entry objects that can then
* be sorted and rendered into options. It is assumed that each element in the collection
* has non-null values for the properties specified for generating the label and value.
*
* @return SKIP_BODY in all cases
* @throws JspException if either the label or value attributes specify properties that are
* not present on the beans in the collection
*/
@Override
public int doStartTag() throws JspException {
if (this.collection == null)
return SKIP_BODY;
String labelProperty = getLabel();
String valueProperty = getValue();
String groupProperty = getGroup();
try {
Locale locale = getPageContext().getRequest().getLocale();
boolean attemptToLocalizeLabels = isAttemptToLocalizeLabels();
for (Object item : this.collection) {
Class<? extends Object> clazz = item.getClass();
// Lookup the bean properties for the label, value and group
Object label = (labelProperty == null) ? item : BeanUtil.getPropertyValue(labelProperty, item);
Object value = (valueProperty == null) ? item : BeanUtil.getPropertyValue(valueProperty, item);
Object group = (groupProperty == null) ? null : BeanUtil.getPropertyValue(groupProperty, item);
if (attemptToLocalizeLabels) {
// Try to localize the label
String packageName = clazz.getPackage() == null ? "" : clazz.getPackage().getName();
String simpleName = LocalizationUtility.getSimpleName(clazz);
String localizedLabel = null;
if (label != null) {
localizedLabel = LocalizationUtility.getLocalizedFieldName
(simpleName + "." + label, packageName, null, locale);
}
if (localizedLabel == null && value != null) {
localizedLabel = LocalizationUtility.getLocalizedFieldName
(simpleName + "." + value, packageName, null, locale);
}
if (localizedLabel != null) label = localizedLabel;
// Try to localize the group
if (group != null) {
String localizedGroup = LocalizationUtility.getLocalizedFieldName(
simpleName + "." + group, packageName, null, locale);
if (localizedGroup != null) group = localizedGroup;
}
}
addEntry(item, label, value, group);
}
}
catch (ExpressionException ee) {
throw new StripesJspException("A problem occurred generating an options-collection. " +
"Most likely either [" + labelProperty + "] or ["+ valueProperty + "] is not a " +
"valid property of the beans in the collection: " + this.collection, ee);
}
return SKIP_BODY;
}
/**
* Optionally sorts the assembled entries and then renders them into a series of
* option tags using an instance of InputOptionTag to do the rendering work.
*
* @return EVAL_PAGE in all cases.
*/
@Override
public int doEndTag() throws JspException {
// Determine if we're going to be sorting the collection
List<Entry> sortedEntries = new LinkedList<Entry>(this.entries);
if (this.sort != null) {
String[] props = StringUtil.standardSplit(this.sort);
for (int i=0;i<props.length;++i) {
if (!props[i].equals("label") && !props[i].equals("value")) {
props[i] = "bean." + props[i];
}
}
Collections.sort(sortedEntries,
new BeanComparator(getPageContext().getRequest().getLocale(), props));
}
InputOptionTag tag = new InputOptionTag();
tag.setParent(this);
tag.setPageContext(getPageContext());
Object lastGroup = null;
JspWriter out = getPageContext().getOut();
for (Entry entry : sortedEntries) {
// Set properties common to all options
tag.getAttributes().putAll(getAttributes());
// Set properties for this tag
tag.setLabel(entry.label == null ? null : entry.label.toString());
tag.setValue(entry.value);
try {
if (entry.group != null && !entry.group.equals(lastGroup))
{
if (lastGroup != null)
optgroupSupport.writeCloseTag(out, "optgroup");
optgroupSupport.set("label", String.valueOf(entry.group));
optgroupSupport.writeOpenTag(out, "optgroup");
lastGroup = entry.group;
}
tag.doStartTag();
tag.doInitBody();
tag.doAfterBody();
tag.doEndTag();
}
catch (Throwable t) {
/** Catch whatever comes back out of the doCatch() method and deal with it */
try { tag.doCatch(t); }
catch (Throwable t2) {
if (t2 instanceof JspException) throw (JspException) t2;
if (t2 instanceof RuntimeException) throw (RuntimeException) t2;
else throw new StripesJspException(t2);
}
}
finally {
tag.doFinally();
}
}
if (lastGroup != null)
optgroupSupport.writeCloseTag(out, "optgroup");
// Clean up any temporary state
this.entries.clear();
return EVAL_PAGE;
}
/**
* Sets the name of the property that will be fetched on each bean in the collection in
* order to generate optgroups. A new optgroup will be created each time the value changes.
*
* @param group the name of the group attribute
*/
public void setGroup(String group) {
this.group = group;
}
/** Gets the property name set with setGroup(). */
public String getGroup() {
return group;
}
}