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.List; 021 022import org.apache.wicket.markup.ComponentTag; 023import org.apache.wicket.markup.MarkupStream; 024import org.apache.wicket.model.IModel; 025import org.apache.wicket.model.util.ListModel; 026import org.apache.wicket.util.convert.IConverter; 027import org.apache.wicket.util.string.AppendingStringBuffer; 028import org.apache.wicket.util.string.Strings; 029 030 031/** 032 * Abstract base class for all choice (html select) options. 033 * <p> 034 * This component uses String concatenation to keep its memory footprint light. 035 * Use Select, SelectOptions and SelectOption from wicket-extensions for more 036 * sophisticated needs. 037 * </p> 038 * 039 * @author Jonathan Locke 040 * @author Eelco Hillenius 041 * @author Johan Compagner 042 * 043 * @param <T> 044 * The model object type 045 * 046 * @param <E> 047 * class of a single element in the choices list 048 */ 049public abstract class AbstractChoice<T, E> extends FormComponent<T> 050{ 051 private static final long serialVersionUID = 1L; 052 053 /** 054 * An enumeration of possible positions of the label for a choice 055 */ 056 public enum LabelPosition 057 { 058 /** 059 * will render the label before the choice 060 */ 061 BEFORE { 062 @Override 063 void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) 064 { 065 buffer.append("<label for=\"") 066 .append(Strings.escapeMarkup(idAttr)) 067 .append('"') 068 .append(extraLabelAttributes) 069 .append('>') 070 .append(renderValue) 071 .append("</label>"); 072 } 073 }, 074 075 /** 076 * will render the label after the choice 077 */ 078 AFTER { 079 @Override 080 void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) 081 { 082 buffer.append("<label for=\"") 083 .append(Strings.escapeMarkup(idAttr)) 084 .append('"') 085 .append(extraLabelAttributes) 086 .append('>') 087 .append(renderValue) 088 .append("</label>"); 089 } 090 }, 091 092 /** 093 * render the label around and the text will be before the the choice 094 */ 095 WRAP_BEFORE { 096 @Override 097 void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) 098 { 099 buffer.append("<label") 100 .append(extraLabelAttributes) 101 .append('>') 102 .append(renderValue) 103 .append(' '); 104 } 105 106 @Override 107 void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) 108 { 109 buffer.append("</label>"); 110 } 111 }, 112 113 /** 114 * render the label around and the text will be after the the choice 115 */ 116 WRAP_AFTER { 117 @Override 118 void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) 119 { 120 buffer.append("<label") 121 .append(extraLabelAttributes) 122 .append('>'); 123 } 124 125 @Override 126 void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) 127 { 128 buffer.append(' ') 129 .append(renderValue) 130 .append("</label>"); 131 } 132 }; 133 134 void before(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) { 135 } 136 137 void after(AppendingStringBuffer buffer, String idAttr, StringBuilder extraLabelAttributes, CharSequence renderValue) { 138 } 139 } 140 141 /** The list of objects. */ 142 private IModel<? extends List<? extends E>> choices; 143 144 /** The renderer used to generate display/id values for the objects. */ 145 private IChoiceRenderer<? super E> renderer; 146 147 /** 148 * Constructor. 149 * 150 * @param id 151 * See Component 152 */ 153 public AbstractChoice(final String id) 154 { 155 this(id, new ListModel<>(new ArrayList<E>()), new ChoiceRenderer<E>()); 156 } 157 158 /** 159 * Constructor. 160 * 161 * @param id 162 * See Component 163 * @param choices 164 * The collection of choices in the dropdown 165 */ 166 public AbstractChoice(final String id, final List<? extends E> choices) 167 { 168 this(id, new ListModel<>(choices), new ChoiceRenderer<E>()); 169 } 170 171 /** 172 * Constructor. 173 * 174 * @param id 175 * See Component 176 * @param renderer 177 * The rendering engine 178 * @param choices 179 * The collection of choices in the dropdown 180 */ 181 public AbstractChoice(final String id, final List<? extends E> choices, 182 final IChoiceRenderer<? super E> renderer) 183 { 184 this(id, new ListModel<>(choices), renderer); 185 } 186 187 /** 188 * Constructor. 189 * 190 * @param id 191 * See Component 192 * @param model 193 * See Component 194 * @param choices 195 * The collection of choices in the dropdown 196 */ 197 public AbstractChoice(final String id, IModel<T> model, final List<? extends E> choices) 198 { 199 this(id, model, new ListModel<>(choices), new ChoiceRenderer<>()); 200 } 201 202 /** 203 * Constructor. 204 * 205 * @param id 206 * See Component 207 * @param model 208 * See Component 209 * @param choices 210 * The drop down choices 211 * @param renderer 212 * The rendering engine 213 */ 214 public AbstractChoice(final String id, IModel<T> model, final List<? extends E> choices, 215 final IChoiceRenderer<? super E> renderer) 216 { 217 this(id, model, new ListModel<>(choices), renderer); 218 } 219 220 /** 221 * Constructor. 222 * 223 * @param id 224 * See Component 225 * @param choices 226 * The collection of choices in the dropdown 227 */ 228 public AbstractChoice(final String id, final IModel<? extends List<? extends E>> choices) 229 { 230 this(id, choices, new ChoiceRenderer<E>()); 231 } 232 233 /** 234 * Constructor. 235 * 236 * @param id 237 * See Component 238 * @param renderer 239 * The rendering engine 240 * @param choices 241 * The collection of choices in the dropdown 242 */ 243 public AbstractChoice(final String id, final IModel<? extends List<? extends E>> choices, 244 final IChoiceRenderer<? super E> renderer) 245 { 246 super(id); 247 this.choices = wrap(choices); 248 setChoiceRenderer(renderer); 249 } 250 251 /** 252 * Constructor. 253 * 254 * @param id 255 * See Component 256 * @param model 257 * See Component 258 * @param choices 259 * The collection of choices in the dropdown 260 */ 261 public AbstractChoice(final String id, IModel<T> model, 262 final IModel<? extends List<? extends E>> choices) 263 { 264 this(id, model, choices, new ChoiceRenderer<>()); 265 } 266 267 /** 268 * Constructor. 269 * 270 * @param id 271 * See Component 272 * @param model 273 * See Component 274 * @param renderer 275 * The rendering engine 276 * @param choices 277 * The drop down choices 278 */ 279 public AbstractChoice(final String id, IModel<T> model, 280 final IModel<? extends List<? extends E>> choices, final IChoiceRenderer<? super E> renderer) 281 { 282 super(id, model); 283 this.choices = wrap(choices); 284 setChoiceRenderer(renderer); 285 } 286 287 /** 288 * @return The collection of object that this choice has 289 */ 290 public final List<? extends E> getChoices() 291 { 292 IModel<? extends List<? extends E>> choicesModel = getChoicesModel(); 293 List<? extends E> choices = (choicesModel != null) ? choicesModel.getObject() : null; 294 if (choices == null) 295 { 296 throw new NullPointerException( 297 "List of choices is null - Was the supplied 'Choices' model empty?"); 298 } 299 return choices; 300 } 301 302 /** 303 * @return The model with the choices for this component 304 */ 305 public IModel<? extends List<? extends E>> getChoicesModel() 306 { 307 return this.choices; 308 } 309 310 /** 311 * Sets the list of choices 312 * 313 * @param choices 314 * model representing the list of choices 315 * @return this for chaining 316 */ 317 public final AbstractChoice<T, E> setChoices(IModel<? extends List<? extends E>> choices) 318 { 319 if (this.choices != null && this.choices != choices) 320 { 321 if (isVersioned()) 322 { 323 addStateChange(); 324 } 325 } 326 this.choices = wrap(choices); 327 return this; 328 } 329 330 /** 331 * Sets the list of choices. 332 * 333 * @param choices 334 * the list of choices 335 * @return this for chaining 336 */ 337 public final AbstractChoice<T, E> setChoices(List<? extends E> choices) 338 { 339 if ((this.choices != null)) 340 { 341 if (isVersioned()) 342 { 343 addStateChange(); 344 } 345 } 346 this.choices = new ListModel<>(choices); 347 return this; 348 } 349 350 /** 351 * @return The IChoiceRenderer used for rendering the data objects 352 */ 353 public final IChoiceRenderer<? super E> getChoiceRenderer() 354 { 355 return renderer; 356 } 357 358 /** 359 * Set the choice renderer to be used. 360 * 361 * @param renderer 362 * The IChoiceRenderer used for rendering the data objects 363 * @return this for chaining 364 */ 365 public final AbstractChoice<T, E> setChoiceRenderer(IChoiceRenderer<? super E> renderer) 366 { 367 if (renderer == null) 368 { 369 renderer = new ChoiceRenderer<>(); 370 } 371 this.renderer = renderer; 372 return this; 373 } 374 375 @Override 376 protected void detachModel() 377 { 378 super.detachModel(); 379 380 if (choices != null) 381 { 382 choices.detach(); 383 } 384 } 385 386 /** 387 * Get a default choice to be rendered additionally to the choices available in the model. 388 * 389 * @param selectedValue 390 * The currently selected value 391 * @return Any default choice, such as "Choose One", depending on the subclass 392 * @see #setChoices(IModel) 393 */ 394 protected CharSequence getDefaultChoice(final String selectedValue) 395 { 396 return ""; 397 } 398 399 /** 400 * Gets whether the given value represents the current selection. 401 * 402 * @param object 403 * The object to check 404 * @param index 405 * The index in the choices collection this object is in. 406 * @param selected 407 * The currently selected string value 408 * @return Whether the given value represents the current selection 409 */ 410 protected abstract boolean isSelected(final E object, int index, String selected); 411 412 /** 413 * Gets whether the given value is disabled. This default implementation always returns false. 414 * 415 * @param object 416 * The object to check 417 * @param index 418 * The index in the choices collection this object is in. 419 * @param selected 420 * The currently selected string value 421 * @return Whether the given value represents the current selection 422 */ 423 protected boolean isDisabled(final E object, int index, String selected) 424 { 425 return false; 426 } 427 428 /** 429 * Handle the container's body. 430 * 431 * @param markupStream 432 * The markup stream 433 * @param openTag 434 * The open tag for the body 435 */ 436 @Override 437 public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) 438 { 439 List<? extends E> choices = getChoices(); 440 final AppendingStringBuffer buffer = new AppendingStringBuffer((choices.size() * 50) + 16); 441 final String selectedValue = getValue(); 442 443 // Append default option 444 buffer.append(getDefaultChoice(selectedValue)); 445 446 for (int index = 0; index < choices.size(); index++) 447 { 448 final E choice = choices.get(index); 449 appendOptionHtml(buffer, choice, index, selectedValue); 450 } 451 452 buffer.append('\n'); 453 replaceComponentTagBody(markupStream, openTag, buffer); 454 } 455 456 /** 457 * Generates and appends html for a single choice into the provided buffer 458 * 459 * @param buffer 460 * Appending string buffer that will have the generated html appended 461 * @param choice 462 * Choice object 463 * @param index 464 * The index of this option 465 * @param selected 466 * The currently selected string value 467 */ 468 protected void appendOptionHtml(AppendingStringBuffer buffer, E choice, int index, String selected) 469 { 470 CharSequence renderValue = renderValue(choice); 471 472 buffer.append("\n<option "); 473 setOptionAttributes(buffer, choice, index, selected); 474 buffer.append('>'); 475 buffer.append(renderValue); 476 buffer.append("</option>"); 477 } 478 479 @SuppressWarnings({ "rawtypes", "unchecked" }) 480 CharSequence renderValue(E choice) 481 { 482 Object objectValue = renderer.getDisplayValue(choice); 483 Class<?> objectClass = (objectValue == null ? null : objectValue.getClass()); 484 485 String displayValue = ""; 486 if (objectClass != null && objectClass != String.class) 487 { 488 IConverter converter = getConverter(objectClass); 489 displayValue = converter.convertToString(objectValue, getLocale()); 490 } 491 else if (objectValue != null) 492 { 493 displayValue = objectValue.toString(); 494 } 495 496 if (localizeDisplayValues()) 497 { 498 String localized = getLocalizer().getString(getId() + "." + displayValue, this, ""); 499 if (Strings.isEmpty(localized)) { 500 localized = getLocalizer().getString(displayValue, this, displayValue); 501 } 502 displayValue = localized; 503 } 504 505 if (getEscapeModelStrings()) 506 { 507 return escapeOptionHtml(displayValue); 508 } 509 510 return displayValue; 511 } 512 513 /** 514 * Sets the attributes of a single choice into the provided buffer. 515 * 516 * @param buffer 517 * Appending string buffer that will have the generated html appended 518 * @param choice 519 * Choice object 520 * @param index 521 * The index of this option 522 * @param selected 523 * The currently selected string value 524 */ 525 protected void setOptionAttributes(AppendingStringBuffer buffer, E choice, int index, String selected) 526 { 527 if (isSelected(choice, index, selected)) 528 { 529 buffer.append("selected=\"selected\" "); 530 } 531 532 if (isDisabled(choice, index, selected)) 533 { 534 buffer.append("disabled=\"disabled\" "); 535 } 536 537 buffer.append("value=\""); 538 buffer.append(Strings.escapeMarkup(renderer.getIdValue(choice, index))); 539 buffer.append('"'); 540 } 541 542 /** 543 * Method to override if you want special escaping of the options html. 544 * 545 * @param displayValue 546 * @return The escaped display value 547 */ 548 protected CharSequence escapeOptionHtml(String displayValue) 549 { 550 return Strings.escapeMarkup(displayValue); 551 } 552 553 /** 554 * Override this method if you want to localize the display values of the generated options. By 555 * default false is returned so that the display values of options are not tested if they have a 556 * i18n key. 557 * 558 * @return true If you want to localize the display values, default == false 559 */ 560 protected boolean localizeDisplayValues() 561 { 562 return false; 563 } 564 565 @Override 566 public final FormComponent<T> setType(Class<?> type) 567 { 568 throw new UnsupportedOperationException( 569 "This class does not support type-conversion because it is performed " 570 + "exclusively by the IChoiceRenderer assigned to this component"); 571 } 572 573 @Override 574 protected void onDetach() 575 { 576 renderer.detach(); 577 578 super.onDetach(); 579 }; 580}