/*
* 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();
};
}