/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.markup.html.form; import java.util.ArrayList; import java.util.List; import org.apache.wicket.markup.ComponentTag; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.model.IModel; import org.apache.wicket.model.util.ListModel; import org.apache.wicket.util.convert.IConverter; import org.apache.wicket.util.string.AppendingStringBuffer; import org.apache.wicket.util.string.Strings; /** * Abstract base class for all choice (html select) options. * <p> * This component uses String concatenation to keep its memory footprint light. * Use Select, SelectOptions and SelectOption from wicket-extensions for more * sophisticated needs. * </p> * * @author Jonathan Locke * @author Eelco Hillenius * @author Johan Compagner * * @param <T> * The model object type * * @param <E> * class of a single element in the choices list */ public abstract class AbstractChoice<T, E> extends FormComponent<T> { private static final long serialVersionUID = 1L; /** * An enumeration of possible positions of the label for a choice */ public enum LabelPosition { /** * will render the label before the choice */ BEFORE, /** * will render the label after the choice */ AFTER, /** * render the label around and the text will be before the the choice */ WRAP_BEFORE, /** * render the label around and the text will be after the the choice */ WRAP_AFTER } /** The list of objects. */ private IModel<? extends List<? extends E>> choices; /** The renderer used to generate display/id values for the objects. */ private IChoiceRenderer<? super E> renderer; /** * Constructor. * * @param id * See Component */ public AbstractChoice(final String id) { this(id, new ListModel<>(new ArrayList<E>()), new ChoiceRenderer<E>()); } /** * Constructor. * * @param id * See Component * @param choices * The collection of choices in the dropdown */ public AbstractChoice(final String id, final List<? extends E> choices) { this(id, new ListModel<>(choices), new ChoiceRenderer<E>()); } /** * Constructor. * * @param id * See Component * @param renderer * The rendering engine * @param choices * The collection of choices in the dropdown */ public AbstractChoice(final String id, final List<? extends E> choices, final IChoiceRenderer<? super E> renderer) { this(id, new ListModel<>(choices), renderer); } /** * Constructor. * * @param id * See Component * @param model * See Component * @param choices * The collection of choices in the dropdown */ public AbstractChoice(final String id, IModel<T> model, final List<? extends E> choices) { this(id, model, new ListModel<>(choices), new ChoiceRenderer<>()); } /** * Constructor. * * @param id * See Component * @param model * See Component * @param choices * The drop down choices * @param renderer * The rendering engine */ public AbstractChoice(final String id, IModel<T> model, final List<? extends E> choices, final IChoiceRenderer<? super E> renderer) { this(id, model, new ListModel<>(choices), renderer); } /** * Constructor. * * @param id * See Component * @param choices * The collection of choices in the dropdown */ public AbstractChoice(final String id, final IModel<? extends List<? extends E>> choices) { this(id, choices, new ChoiceRenderer<E>()); } /** * Constructor. * * @param id * See Component * @param renderer * The rendering engine * @param choices * The collection of choices in the dropdown */ public AbstractChoice(final String id, final IModel<? extends List<? extends E>> choices, final IChoiceRenderer<? super E> renderer) { super(id); this.choices = wrap(choices); setChoiceRenderer(renderer); } /** * Constructor. * * @param id * See Component * @param model * See Component * @param choices * The collection of choices in the dropdown */ public AbstractChoice(final String id, IModel<T> model, final IModel<? extends List<? extends E>> choices) { this(id, model, choices, new ChoiceRenderer<>()); } /** * Constructor. * * @param id * See Component * @param model * See Component * @param renderer * The rendering engine * @param choices * The drop down choices */ public AbstractChoice(final String id, IModel<T> model, final IModel<? extends List<? extends E>> choices, final IChoiceRenderer<? super E> renderer) { super(id, model); this.choices = wrap(choices); setChoiceRenderer(renderer); } /** * @return The collection of object that this choice has */ public final List<? extends E> getChoices() { IModel<? extends List<? extends E>> choicesModel = getChoicesModel(); List<? extends E> choices = (choicesModel != null) ? choicesModel.getObject() : null; if (choices == null) { throw new NullPointerException( "List of choices is null - Was the supplied 'Choices' model empty?"); } return choices; } /** * @return The model with the choices for this component */ public IModel<? extends List<? extends E>> getChoicesModel() { return this.choices; } /** * Sets the list of choices * * @param choices * model representing the list of choices * @return this for chaining */ public final AbstractChoice<T, E> setChoices(IModel<? extends List<? extends E>> choices) { if (this.choices != null && this.choices != choices) { if (isVersioned()) { addStateChange(); } } this.choices = wrap(choices); return this; } /** * Sets the list of choices. * * @param choices * the list of choices * @return this for chaining */ public final AbstractChoice<T, E> setChoices(List<? extends E> choices) { if ((this.choices != null)) { if (isVersioned()) { addStateChange(); } } this.choices = new ListModel<>(choices); return this; } /** * @return The IChoiceRenderer used for rendering the data objects */ public final IChoiceRenderer<? super E> getChoiceRenderer() { return renderer; } /** * Set the choice renderer to be used. * * @param renderer * The IChoiceRenderer used for rendering the data objects * @return this for chaining */ public final AbstractChoice<T, E> setChoiceRenderer(IChoiceRenderer<? super E> renderer) { if (renderer == null) { renderer = new ChoiceRenderer<>(); } this.renderer = renderer; return this; } @Override protected void detachModel() { super.detachModel(); if (choices != null) { choices.detach(); } } /** * Get a default choice to be rendered additionally to the choices available in the model. * * @param selectedValue * The currently selected value * @return Any default choice, such as "Choose One", depending on the subclass * @see #setChoices(IModel) */ protected CharSequence getDefaultChoice(final String selectedValue) { return ""; } /** * Gets whether the given value represents the current selection. * * @param object * The object to check * @param index * The index in the choices collection this object is in. * @param selected * The currently selected string value * @return Whether the given value represents the current selection */ protected abstract boolean isSelected(final E object, int index, String selected); /** * Gets whether the given value is disabled. This default implementation always returns false. * * @param object * The object to check * @param index * The index in the choices collection this object is in. * @param selected * The currently selected string value * @return Whether the given value represents the current selection */ protected boolean isDisabled(final E object, int index, String selected) { return false; } /** * Handle the container's body. * * @param markupStream * The markup stream * @param openTag * The open tag for the body */ @Override public void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) { List<? extends E> choices = getChoices(); final AppendingStringBuffer buffer = new AppendingStringBuffer((choices.size() * 50) + 16); final String selectedValue = getValue(); // Append default option buffer.append(getDefaultChoice(selectedValue)); for (int index = 0; index < choices.size(); index++) { final E choice = choices.get(index); appendOptionHtml(buffer, choice, index, selectedValue); } buffer.append('\n'); replaceComponentTagBody(markupStream, openTag, buffer); } /** * Generates and appends html for a single choice into the provided buffer * * @param buffer * Appending string buffer that will have the generated html appended * @param choice * Choice object * @param index * The index of this option * @param selected * The currently selected string value */ @SuppressWarnings("unchecked") protected void appendOptionHtml(AppendingStringBuffer buffer, E choice, int index, String selected) { Object objectValue = renderer.getDisplayValue(choice); Class<?> objectClass = (objectValue == null ? null : objectValue.getClass()); String displayValue = ""; if (objectClass != null && objectClass != String.class) { @SuppressWarnings("rawtypes") IConverter converter = getConverter(objectClass); displayValue = converter.convertToString(objectValue, getLocale()); } else if (objectValue != null) { displayValue = objectValue.toString(); } buffer.append("\n<option "); setOptionAttributes(buffer, choice, index, selected); buffer.append('>'); String display = displayValue; if (localizeDisplayValues()) { display = getLocalizer().getString(displayValue, this, displayValue); } CharSequence escaped = display; if (getEscapeModelStrings()) { escaped = escapeOptionHtml(display); } buffer.append(escaped); buffer.append("</option>"); } /** * Sets the attributes of a single choice into the provided buffer. * * @param buffer * Appending string buffer that will have the generated html appended * @param choice * Choice object * @param index * The index of this option * @param selected * The currently selected string value */ protected void setOptionAttributes(AppendingStringBuffer buffer, E choice, int index, String selected) { if (isSelected(choice, index, selected)) { buffer.append("selected=\"selected\" "); } if (isDisabled(choice, index, selected)) { buffer.append("disabled=\"disabled\" "); } buffer.append("value=\""); buffer.append(Strings.escapeMarkup(renderer.getIdValue(choice, index))); buffer.append('"'); } /** * Method to override if you want special escaping of the options html. * * @param displayValue * @return The escaped display value */ protected CharSequence escapeOptionHtml(String displayValue) { return Strings.escapeMarkup(displayValue); } /** * Override this method if you want to localize the display values of the generated options. By * default false is returned so that the display values of options are not tested if they have a * i18n key. * * @return true If you want to localize the display values, default == false */ protected boolean localizeDisplayValues() { return false; } @Override public final FormComponent<T> setType(Class<?> type) { throw new UnsupportedOperationException( "This class does not support type-conversion because it is performed " + "exclusively by the IChoiceRenderer assigned to this component"); } @Override protected void onDetach() { renderer.detach(); super.onDetach(); }; }