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.markup.html.form; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.StringTokenizer; 025 026import org.apache.wicket.MetaDataKey; 027import org.apache.wicket.markup.ComponentTag; 028import org.apache.wicket.model.IModel; 029import org.apache.wicket.util.convert.ConversionException; 030import org.apache.wicket.util.string.AppendingStringBuffer; 031import org.apache.wicket.util.string.Strings; 032 033 034/** 035 * A multiple choice list component. 036 * 037 * @author Jonathan Locke 038 * @author Johan Compagner 039 * @author Martijn Dashorst 040 * 041 * @param <T> 042 * The model object type 043 */ 044public class ListMultipleChoice<T> extends AbstractChoice<Collection<T>, T> 045{ 046 private static final long serialVersionUID = 1L; 047 048 /** Meta key for the retain disabled flag */ 049 static MetaDataKey<Boolean> RETAIN_DISABLED_META_KEY = new MetaDataKey<>() 050 { 051 private static final long serialVersionUID = 1L; 052 }; 053 054 /** The default maximum number of rows to display. */ 055 private static final int DEFAULT_MAX_ROWS = 8; 056 057 /** The maximum number of rows to display. */ 058 private int maxRows = DEFAULT_MAX_ROWS; 059 060 /** 061 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String) 062 */ 063 public ListMultipleChoice(final String id) 064 { 065 super(id); 066 } 067 068 /** 069 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, List) 070 */ 071 public ListMultipleChoice(final String id, final List<? extends T> choices) 072 { 073 super(id, choices); 074 } 075 076 /** 077 * Creates a multiple choice list with a maximum number of visible rows. 078 * 079 * @param id 080 * component id 081 * @param choices 082 * list of choices 083 * @param maxRows 084 * the maximum number of visible rows. 085 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, List) 086 */ 087 public ListMultipleChoice(final String id, final List<? extends T> choices, final int maxRows) 088 { 089 super(id, choices); 090 this.maxRows = maxRows; 091 } 092 093 /** 094 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, 095 * List,IChoiceRenderer) 096 */ 097 public ListMultipleChoice(final String id, final List<? extends T> choices, 098 final IChoiceRenderer<? super T> renderer) 099 { 100 super(id, choices, renderer); 101 } 102 103 /** 104 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel, List) 105 * 106 * @param id 107 * @param object 108 * @param choices 109 */ 110 @SuppressWarnings("unchecked") 111 public ListMultipleChoice(final String id, IModel<? extends Collection<T>> object, 112 final List<? extends T> choices) 113 { 114 super(id, (IModel<Collection<T>>)object, choices); 115 } 116 117 /** 118 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel, 119 * List,IChoiceRenderer) 120 * 121 * @param id 122 * @param object 123 * @param choices 124 * @param renderer 125 */ 126 @SuppressWarnings("unchecked") 127 public ListMultipleChoice(final String id, IModel<? extends Collection<T>> object, 128 final List<? extends T> choices, final IChoiceRenderer<? super T> renderer) 129 { 130 super(id, (IModel<Collection<T>>)object, choices, renderer); 131 } 132 133 /** 134 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel) 135 */ 136 public ListMultipleChoice(String id, IModel<? extends List<? extends T>> choices) 137 { 138 super(id, choices); 139 } 140 141 /** 142 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel,IModel) 143 * 144 * @param id 145 * @param model 146 * @param choices 147 */ 148 @SuppressWarnings("unchecked") 149 public ListMultipleChoice(String id, IModel<? extends Collection<T>> model, 150 IModel<? extends List<? extends T>> choices) 151 { 152 super(id, (IModel<Collection<T>>)model, choices); 153 } 154 155 /** 156 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, 157 * IModel,IChoiceRenderer) 158 */ 159 public ListMultipleChoice(String id, IModel<? extends List<? extends T>> choices, 160 IChoiceRenderer<? super T> renderer) 161 { 162 super(id, choices, renderer); 163 } 164 165 166 /** 167 * @see org.apache.wicket.markup.html.form.AbstractChoice#AbstractChoice(String, IModel, 168 * IModel,IChoiceRenderer) 169 * 170 * @param id 171 * @param model 172 * @param choices 173 * @param renderer 174 */ 175 @SuppressWarnings("unchecked") 176 public ListMultipleChoice(String id, IModel<? extends Collection<T>> model, 177 IModel<? extends List<? extends T>> choices, IChoiceRenderer<? super T> renderer) 178 { 179 super(id, (IModel<Collection<T>>)model, choices, renderer); 180 } 181 182 /** 183 * Sets the number of visible rows in the listbox. 184 * 185 * @param maxRows 186 * the number of visible rows 187 * @return this 188 */ 189 public final ListMultipleChoice<T> setMaxRows(final int maxRows) 190 { 191 this.maxRows = maxRows; 192 return this; 193 } 194 195 /** 196 * @see FormComponent#getModelValue() 197 */ 198 @Override 199 public final String getModelValue() 200 { 201 final AppendingStringBuffer buffer = new AppendingStringBuffer(); 202 203 final Collection<T> selectedValues = getModelObject(); 204 if (selectedValues != null) 205 { 206 final List<? extends T> choices = getChoices(); 207 for (T object : selectedValues) 208 { 209 if (buffer.length() > 0) 210 { 211 buffer.append(VALUE_SEPARATOR); 212 } 213 int index = choices.indexOf(object); 214 buffer.append(getChoiceRenderer().getIdValue(object, index)); 215 } 216 } 217 218 return buffer.toString(); 219 } 220 221 /** 222 * @see org.apache.wicket.markup.html.form.AbstractChoice#isSelected(Object,int, String) 223 */ 224 @Override 225 protected final boolean isSelected(T choice, int index, String selected) 226 { 227 // Have a value at all? 228 if (selected != null) 229 { 230 String idValue = getChoiceRenderer().getIdValue(choice, index); 231 // Loop through ids 232 for (final StringTokenizer tokenizer = new StringTokenizer(selected, VALUE_SEPARATOR); tokenizer.hasMoreTokens();) 233 { 234 final String id = tokenizer.nextToken(); 235 if (id.equals(idValue)) 236 { 237 return true; 238 } 239 } 240 } 241 return false; 242 } 243 244 /** 245 * @see org.apache.wicket.Component#onComponentTag(ComponentTag) 246 */ 247 @Override 248 protected void onComponentTag(final ComponentTag tag) 249 { 250 super.onComponentTag(tag); 251 tag.put("multiple", "multiple"); 252 253 if (!tag.getAttributes().containsKey("size")) 254 { 255 tag.put("size", Math.min(maxRows, getChoices().size())); 256 } 257 } 258 259 /** 260 * @see org.apache.wicket.markup.html.form.FormComponent#convertValue(String[]) 261 */ 262 @Override 263 protected Collection<T> convertValue(String[] ids) throws ConversionException 264 { 265 if (ids != null && ids.length > 0 && !Strings.isEmpty(ids[0])) 266 { 267 return convertChoiceIdsToChoices(ids); 268 } 269 else 270 { 271 ArrayList<T> result = new ArrayList<>(); 272 addRetainedDisabled(result); 273 return result; 274 } 275 } 276 277 /** 278 * Converts submitted choice ids to choice objects. 279 * 280 * @param ids 281 * choice ids. this array is nonnull and always contains at least one id. 282 * @return list of choices. 283 */ 284 protected List<T> convertChoiceIdsToChoices(String[] ids) 285 { 286 ArrayList<T> selectedValues = new ArrayList<>(); 287 288 // If one or more ids is selected 289 if (ids != null && ids.length > 0 && !Strings.isEmpty(ids[0])) 290 { 291 // Get values that could be selected 292 final Map<String, T> choiceIds2choiceValues = createChoicesIdsMap(); 293 294 // Loop through selected indices 295 for (String id : ids) 296 { 297 if (choiceIds2choiceValues.containsKey(id)) 298 { 299 selectedValues.add(choiceIds2choiceValues.get(id)); 300 } 301 } 302 } 303 addRetainedDisabled(selectedValues); 304 305 return selectedValues; 306 307 } 308 309 /** 310 * Creates a map of choice IDs to choice values. This map can be used to speed up lookups e.g. 311 * in {@link #convertChoiceIdsToChoices(String[])}. <strong>Do not store the result of this 312 * method.</strong> The choices list can change between requests so this map <em>must</em> be 313 * regenerated. 314 * 315 * @return a map. 316 */ 317 private Map<String, T> createChoicesIdsMap() 318 { 319 final List<? extends T> choices = getChoices(); 320 321 final Map<String, T> choiceIds2choiceValues = new HashMap<String, T>(choices.size(), 1); 322 323 for (int index = 0; index < choices.size(); index++) 324 { 325 // Get next choice 326 final T choice = choices.get(index); 327 choiceIds2choiceValues.put(getChoiceRenderer().getIdValue(choice, index), choice); 328 } 329 return choiceIds2choiceValues; 330 } 331 332 private void addRetainedDisabled(ArrayList<T> selectedValues) 333 { 334 if (isRetainDisabledSelected()) 335 { 336 Collection<T> unchangedModel = getModelObject(); 337 String selected; 338 { 339 StringBuilder builder = new StringBuilder(); 340 for (T t : unchangedModel) 341 { 342 builder.append(t); 343 builder.append(';'); 344 } 345 selected = builder.toString(); 346 } 347 List<? extends T> choices = getChoices(); 348 for (int i = 0; i < choices.size(); i++) 349 { 350 final T choice = choices.get(i); 351 if (isDisabled(choice, i, selected)) 352 { 353 if (unchangedModel.contains(choice)) 354 { 355 if (!selectedValues.contains(choice)) 356 { 357 selectedValues.add(choice); 358 } 359 } 360 } 361 } 362 } 363 } 364 365 /** 366 * See {@link FormComponent#updateCollectionModel(FormComponent)} for details on how the model 367 * is updated. 368 */ 369 @Override 370 public void updateModel() 371 { 372 FormComponent.updateCollectionModel(this); 373 } 374 375 /** 376 * If true, choices that were selected in the model but disabled in rendering will be retained 377 * in the model after a form submit. Example: Choices are [1, 2, 3, 4]. Model collection is [2, 378 * 4]. In rendering, choices 2 and 3 are disabled ({@link #isDisabled(Object, int, String)}). 379 * That means that four checkboxes are rendered, checkboxes 2 and 4 are checked, but 2 and 3 are 380 * not clickable. User checks 1 and unchecks 4. If this flag is off, the model will be updated 381 * to [1]. This is because the browser does not re-submit a disabled checked checkbox: it only 382 * submits [1]. Therefore Wicket will only see the [1] and update the model accordingly. If you 383 * set this flag to true, Wicket will check the model before updating to find choices that were 384 * selected but disabled. These choices will then be retained, leading to a new model value of 385 * [1, 2] as (probably) expected by the user. Note that this will lead to additional calls to 386 * {@link #isDisabled(Object, int, String)}. 387 * 388 * @return flag 389 */ 390 public boolean isRetainDisabledSelected() 391 { 392 Boolean flag = getMetaData(RETAIN_DISABLED_META_KEY); 393 return (flag != null && flag); 394 } 395 396 /** 397 * @param retain 398 * flag 399 * @return this 400 * @see #isRetainDisabledSelected() 401 */ 402 public ListMultipleChoice<T> setRetainDisabledSelected(boolean retain) 403 { 404 setMetaData(RETAIN_DISABLED_META_KEY, (retain) ? true : null); 405 return this; 406 } 407}