001/* 002 * Copyright 2002-2012 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.tags.form; 018 019import java.util.Collection; 020import java.util.Iterator; 021import java.util.Map; 022import javax.servlet.jsp.JspException; 023 024import org.springframework.beans.BeanWrapper; 025import org.springframework.beans.PropertyAccessorFactory; 026import org.springframework.util.Assert; 027import org.springframework.util.ObjectUtils; 028import org.springframework.util.StringUtils; 029 030/** 031 * Abstract base class to provide common methods for implementing 032 * databinding-aware JSP tags for rendering <i>multiple</i> 033 * HTML '{@code input}' elements with a '{@code type}' 034 * of '{@code checkbox}' or '{@code radio}'. 035 * 036 * @author Juergen Hoeller 037 * @author Scott Andrews 038 * @since 2.5.2 039 */ 040@SuppressWarnings("serial") 041public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElementTag { 042 043 /** 044 * The HTML '{@code span}' tag. 045 */ 046 private static final String SPAN_TAG = "span"; 047 048 049 /** 050 * The {@link java.util.Collection}, {@link java.util.Map} or array of objects 051 * used to generate the '{@code input type="checkbox/radio"}' tags. 052 */ 053 private Object items; 054 055 /** 056 * The name of the property mapped to the '{@code value}' attribute 057 * of the '{@code input type="checkbox/radio"}' tag. 058 */ 059 private String itemValue; 060 061 /** 062 * The value to be displayed as part of the '{@code input type="checkbox/radio"}' tag. 063 */ 064 private String itemLabel; 065 066 /** 067 * The HTML element used to enclose the '{@code input type="checkbox/radio"}' tag. 068 */ 069 private String element = SPAN_TAG; 070 071 /** 072 * Delimiter to use between each '{@code input type="checkbox/radio"}' tags. 073 */ 074 private String delimiter; 075 076 077 /** 078 * Set the {@link java.util.Collection}, {@link java.util.Map} or array of objects 079 * used to generate the '{@code input type="checkbox/radio"}' tags. 080 * <p>Typically a runtime expression. 081 * @param items said items 082 */ 083 public void setItems(Object items) { 084 Assert.notNull(items, "'items' must not be null"); 085 this.items = items; 086 } 087 088 /** 089 * Get the {@link java.util.Collection}, {@link java.util.Map} or array of objects 090 * used to generate the '{@code input type="checkbox/radio"}' tags. 091 */ 092 protected Object getItems() { 093 return this.items; 094 } 095 096 /** 097 * Set the name of the property mapped to the '{@code value}' attribute 098 * of the '{@code input type="checkbox/radio"}' tag. 099 * <p>May be a runtime expression. 100 */ 101 public void setItemValue(String itemValue) { 102 Assert.hasText(itemValue, "'itemValue' must not be empty"); 103 this.itemValue = itemValue; 104 } 105 106 /** 107 * Get the name of the property mapped to the '{@code value}' attribute 108 * of the '{@code input type="checkbox/radio"}' tag. 109 */ 110 protected String getItemValue() { 111 return this.itemValue; 112 } 113 114 /** 115 * Set the value to be displayed as part of the 116 * '{@code input type="checkbox/radio"}' tag. 117 * <p>May be a runtime expression. 118 */ 119 public void setItemLabel(String itemLabel) { 120 Assert.hasText(itemLabel, "'itemLabel' must not be empty"); 121 this.itemLabel = itemLabel; 122 } 123 124 /** 125 * Get the value to be displayed as part of the 126 * '{@code input type="checkbox/radio"}' tag. 127 */ 128 protected String getItemLabel() { 129 return this.itemLabel; 130 } 131 132 /** 133 * Set the delimiter to be used between each 134 * '{@code input type="checkbox/radio"}' tag. 135 * <p>By default, there is <em>no</em> delimiter. 136 */ 137 public void setDelimiter(String delimiter) { 138 this.delimiter = delimiter; 139 } 140 141 /** 142 * Return the delimiter to be used between each 143 * '{@code input type="radio"}' tag. 144 */ 145 public String getDelimiter() { 146 return this.delimiter; 147 } 148 149 /** 150 * Set the HTML element used to enclose the 151 * '{@code input type="checkbox/radio"}' tag. 152 * <p>Defaults to an HTML '{@code <span/>}' tag. 153 */ 154 public void setElement(String element) { 155 Assert.hasText(element, "'element' cannot be null or blank"); 156 this.element = element; 157 } 158 159 /** 160 * Get the HTML element used to enclose 161 * '{@code input type="checkbox/radio"}' tag. 162 */ 163 public String getElement() { 164 return this.element; 165 } 166 167 168 /** 169 * Appends a counter to a specified id as well, 170 * since we're dealing with multiple HTML elements. 171 */ 172 @Override 173 protected String resolveId() throws JspException { 174 Object id = evaluate("id", getId()); 175 if (id != null) { 176 String idString = id.toString(); 177 return (StringUtils.hasText(idString) ? TagIdGenerator.nextId(idString, this.pageContext) : null); 178 } 179 return autogenerateId(); 180 } 181 182 /** 183 * Renders the '{@code input type="radio"}' element with the configured 184 * {@link #setItems(Object)} values. Marks the element as checked if the 185 * value matches the bound value. 186 */ 187 @Override 188 @SuppressWarnings("rawtypes") 189 protected int writeTagContent(TagWriter tagWriter) throws JspException { 190 Object items = getItems(); 191 Object itemsObject = (items instanceof String ? evaluate("items", items) : items); 192 193 String itemValue = getItemValue(); 194 String itemLabel = getItemLabel(); 195 String valueProperty = 196 (itemValue != null ? ObjectUtils.getDisplayString(evaluate("itemValue", itemValue)) : null); 197 String labelProperty = 198 (itemLabel != null ? ObjectUtils.getDisplayString(evaluate("itemLabel", itemLabel)) : null); 199 200 Class<?> boundType = getBindStatus().getValueType(); 201 if (itemsObject == null && boundType != null && boundType.isEnum()) { 202 itemsObject = boundType.getEnumConstants(); 203 } 204 205 if (itemsObject == null) { 206 throw new IllegalArgumentException("Attribute 'items' is required and must be a Collection, an Array or a Map"); 207 } 208 209 if (itemsObject.getClass().isArray()) { 210 Object[] itemsArray = (Object[]) itemsObject; 211 for (int i = 0; i < itemsArray.length; i++) { 212 Object item = itemsArray[i]; 213 writeObjectEntry(tagWriter, valueProperty, labelProperty, item, i); 214 } 215 } 216 else if (itemsObject instanceof Collection) { 217 final Collection<?> optionCollection = (Collection<?>) itemsObject; 218 int itemIndex = 0; 219 for (Iterator<?> it = optionCollection.iterator(); it.hasNext(); itemIndex++) { 220 Object item = it.next(); 221 writeObjectEntry(tagWriter, valueProperty, labelProperty, item, itemIndex); 222 } 223 } 224 else if (itemsObject instanceof Map) { 225 final Map<?, ?> optionMap = (Map<?, ?>) itemsObject; 226 int itemIndex = 0; 227 for (Iterator it = optionMap.entrySet().iterator(); it.hasNext(); itemIndex++) { 228 Map.Entry entry = (Map.Entry) it.next(); 229 writeMapEntry(tagWriter, valueProperty, labelProperty, entry, itemIndex); 230 } 231 } 232 else { 233 throw new IllegalArgumentException("Attribute 'items' must be an array, a Collection or a Map"); 234 } 235 236 return SKIP_BODY; 237 } 238 239 private void writeObjectEntry(TagWriter tagWriter, String valueProperty, 240 String labelProperty, Object item, int itemIndex) throws JspException { 241 242 BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(item); 243 Object renderValue; 244 if (valueProperty != null) { 245 renderValue = wrapper.getPropertyValue(valueProperty); 246 } 247 else if (item instanceof Enum) { 248 renderValue = ((Enum<?>) item).name(); 249 } 250 else { 251 renderValue = item; 252 } 253 Object renderLabel = (labelProperty != null ? wrapper.getPropertyValue(labelProperty) : item); 254 writeElementTag(tagWriter, item, renderValue, renderLabel, itemIndex); 255 } 256 257 private void writeMapEntry(TagWriter tagWriter, String valueProperty, 258 String labelProperty, Map.Entry<?, ?> entry, int itemIndex) throws JspException { 259 260 Object mapKey = entry.getKey(); 261 Object mapValue = entry.getValue(); 262 BeanWrapper mapKeyWrapper = PropertyAccessorFactory.forBeanPropertyAccess(mapKey); 263 BeanWrapper mapValueWrapper = PropertyAccessorFactory.forBeanPropertyAccess(mapValue); 264 Object renderValue = (valueProperty != null ? 265 mapKeyWrapper.getPropertyValue(valueProperty) : mapKey.toString()); 266 Object renderLabel = (labelProperty != null ? 267 mapValueWrapper.getPropertyValue(labelProperty) : mapValue.toString()); 268 writeElementTag(tagWriter, mapKey, renderValue, renderLabel, itemIndex); 269 } 270 271 private void writeElementTag(TagWriter tagWriter, Object item, Object value, Object label, int itemIndex) 272 throws JspException { 273 274 tagWriter.startTag(getElement()); 275 if (itemIndex > 0) { 276 Object resolvedDelimiter = evaluate("delimiter", getDelimiter()); 277 if (resolvedDelimiter != null) { 278 tagWriter.appendValue(resolvedDelimiter.toString()); 279 } 280 } 281 tagWriter.startTag("input"); 282 String id = resolveId(); 283 writeOptionalAttribute(tagWriter, "id", id); 284 writeOptionalAttribute(tagWriter, "name", getName()); 285 writeOptionalAttributes(tagWriter); 286 tagWriter.writeAttribute("type", getInputType()); 287 renderFromValue(item, value, tagWriter); 288 tagWriter.endTag(); 289 tagWriter.startTag("label"); 290 tagWriter.writeAttribute("for", id); 291 tagWriter.appendValue(convertToDisplayString(label)); 292 tagWriter.endTag(); 293 tagWriter.endTag(); 294 } 295 296}