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