001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.extensions.markup.html.form.select; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.List; 023 024import org.apache.wicket.WicketRuntimeException; 025import org.apache.wicket.markup.html.WebMarkupContainer; 026import org.apache.wicket.markup.html.form.FormComponent; 027import org.apache.wicket.model.IModel; 028import org.apache.wicket.util.lang.Args; 029import org.apache.wicket.util.lang.Objects; 030import org.apache.wicket.util.string.Strings; 031import org.apache.wicket.util.visit.IVisit; 032import org.apache.wicket.util.visit.IVisitor; 033 034 035/** 036 * Component that represents a <code><select></code> box. Elements are provided by one or more 037 * <code>SelectOptions</code> components in the hierarchy below the <code>Select</code> component. 038 * 039 * Advantages to the standard choice components is that the user has a lot more control over the 040 * markup between the <select> tag and its children <option> tags: allowing for such 041 * things as <optgroup> tags. 042 * 043 * <p> 044 * Example HTML: 045 * 046 * <pre> 047 * <select wicket:id="select" multiple="multiple"> 048 * <wicket:container wicket:id="options"> 049 * <option wicket:id="option">Option Label</option> 050 * </wicket:container> 051 * </select> 052 * </pre> 053 * 054 * Related Java Code: 055 * 056 * <pre> 057 * Select select = new Select("select", selectionModel); 058 * add(select); 059 * SelectOptions options = new SelectOptions("options", elements, renderer); 060 * select.add(options); 061 * </pre> 062 * 063 * Note that you don't need to add component(s) for the <option> tag - they are created by 064 * SelectOptions 065 * </p> 066 * <p> 067 * <strong>Note</strong>: due to the usage of a SelectOption for each <option> the memory 068 * footprint of the page will grow with the number of <option>s you need. Consider using 069 * {@link org.apache.wicket.markup.html.form.DropDownChoice} component if it is able to fulfill your 070 * requirements. 071 * </p> 072 * 073 * @see SelectOption 074 * @see SelectOptions 075 * @see org.apache.wicket.markup.html.form.DropDownChoice 076 * 077 * @author Igor Vaynberg 078 * @param <T> 079 */ 080public class Select<T> extends FormComponent<T> 081{ 082 private static final long serialVersionUID = 1L; 083 084 /** 085 * Constructor that will create a default model collection 086 * 087 * @param id 088 * component id 089 */ 090 public Select(final String id) 091 { 092 super(id); 093 } 094 095 /** 096 * @param id 097 * @param model 098 * @see WebMarkupContainer#WebMarkupContainer(String, IModel) 099 */ 100 public Select(final String id, final IModel<T> model) 101 { 102 super(id, model); 103 } 104 105 @Override 106 protected String getModelValue() 107 { 108 final StringBuilder builder = new StringBuilder(); 109 110 visitChildren(SelectOption.class, new IVisitor<SelectOption<T>, Void>() 111 { 112 @Override 113 public void component(SelectOption<T> option, IVisit<Void> visit) 114 { 115 if (isSelected(option.getDefaultModel())) 116 { 117 if (builder.length() > 0) 118 { 119 builder.append(VALUE_SEPARATOR); 120 } 121 builder.append(option.getValue()); 122 } 123 } 124 }); 125 126 return builder.toString(); 127 } 128 129 @Override 130 public void convertInput() 131 { 132 boolean supportsMultiple = getModelObject() instanceof Collection; 133 134 /* 135 * + * the input contains an array of values of the selected option components unless 136 * nothing was selected in which case the input contains null 137 */ 138 String[] values = getInputAsArray(); 139 140 if ((values == null) || (values.length == 0)) 141 { 142 setConvertedInput(null); 143 return; 144 } 145 146 if (!supportsMultiple && (values.length > 1)) 147 { 148 throw new WicketRuntimeException( 149 "The model of Select component [" + 150 getPath() + 151 "] is not of type java.util.Collection, but more then one SelectOption component has been selected. Either remove the multiple attribute from the select tag or make the model of the Select component a collection"); 152 } 153 154 List<Object> converted = new ArrayList<>(values.length); 155 156 /* 157 * if the input is null we do not need to do anything since the model collection has already 158 * been cleared 159 */ 160 for (int i = 0; i < values.length; i++) 161 { 162 final String value = values[i]; 163 if (!Strings.isEmpty(value)) 164 { 165 SelectOption<T> option = visitChildren(SelectOption.class, 166 new IVisitor<SelectOption<T>, SelectOption<T>>() 167 { 168 @Override 169 public void component(SelectOption<T> option, IVisit<SelectOption<T>> visit) 170 { 171 if (String.valueOf(option.getValue()).equals(value)) 172 { 173 visit.stop(option); 174 } 175 } 176 }); 177 178 if (option == null) 179 { 180 throw new WicketRuntimeException( 181 "submitted http post value [" + 182 Arrays.toString(values) + 183 "] for SelectOption component [" + 184 getPath() + 185 "] contains an illegal value [" + 186 value + 187 "] which does not point to a SelectOption component. Due to this the Select component cannot resolve the selected SelectOption component pointed to by the illegal value. A possible reason is that component hierarchy changed between rendering and form submission."); 188 } 189 converted.add(option.getDefaultModelObject()); 190 } 191 } 192 193 if (converted.isEmpty()) 194 { 195 setConvertedInput(null); 196 } 197 else if (!supportsMultiple) 198 { 199 @SuppressWarnings("unchecked") 200 T convertedInput = (T)converted.get(0); 201 setConvertedInput(convertedInput); 202 } 203 else 204 { 205 @SuppressWarnings("unchecked") 206 T convertedInput = (T)converted; 207 setConvertedInput(convertedInput); 208 } 209 } 210 211 /** 212 * @see FormComponent#updateModel() 213 */ 214 @SuppressWarnings({ "unchecked", "rawtypes" }) 215 @Override 216 public void updateModel() 217 { 218 T object = getModelObject(); 219 boolean supportsMultiple = object instanceof Collection; 220 221 Object converted = getConvertedInput(); 222 /* 223 * update the model 224 */ 225 if (supportsMultiple) 226 { 227 Collection<?> modelCollection = (Collection<?>)object; 228 modelChanging(); 229 modelCollection.clear(); 230 if (converted != null) 231 { 232 modelCollection.addAll((Collection)converted); 233 } 234 modelChanged(); 235 // force notify of model update via setObject() 236 setDefaultModelObject(modelCollection); 237 } 238 else 239 { 240 setDefaultModelObject(converted); 241 } 242 } 243 244 /** 245 * Checks if the specified option is selected based on raw input 246 * 247 * @param option 248 * @return {@code true} if the option is selected, {@code false} otherwise 249 */ 250 boolean isSelected(final SelectOption<?> option) 251 { 252 Args.notNull(option, "option"); 253 254 // if the raw input is specified use that, otherwise use model 255 if (hasRawInput()) 256 { 257 final String raw = getRawInput(); 258 if (!Strings.isEmpty(raw)) 259 { 260 String[] values = raw.split(VALUE_SEPARATOR); 261 for (int i = 0; i < values.length; i++) 262 { 263 String value = values[i]; 264 if (value.equals(option.getValue())) 265 { 266 return true; 267 } 268 } 269 } 270 return false; 271 } 272 273 return isSelected(option.getDefaultModel()); 274 } 275 276 /** 277 * Does the given model contain a selected value. 278 * 279 * @param model 280 * model to test on selection 281 * @return selected 282 */ 283 protected boolean isSelected(IModel<?> model) 284 { 285 return compareModels(getDefaultModelObject(), model.getObject()); 286 } 287 288 private boolean compareModels(final Object selected, final Object value) 289 { 290 if ((selected != null) && (selected instanceof Collection)) 291 { 292 if (value instanceof Collection) 293 { 294 return ((Collection<?>)selected).containsAll((Collection<?>)value); 295 } 296 else 297 { 298 return ((Collection<?>)selected).contains(value); 299 } 300 } 301 else 302 { 303 return Objects.equal(selected, value); 304 } 305 } 306 307}