/*
* 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.extensions.markup.html.form.select;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Objects;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.visit.IVisit;
import org.apache.wicket.util.visit.IVisitor;
/**
* Component that represents a <code><select></code> box. Elements are provided by one or more
* <code>SelectOptions</code> components in the hierarchy below the <code>Select</code> component.
*
* Advantages to the standard choice components is that the user has a lot more control over the
* markup between the <select> tag and its children <option> tags: allowing for such
* things as <optgroup> tags.
*
* <p>
* Example HTML:
*
* <pre>
* <select wicket:id="select" multiple="multiple">
* <wicket:container wicket:id="options">
* <option wicket:id="option">Option Label</option>
* </wicket:container>
* </select>
* </pre>
*
* Related Java Code:
*
* <pre>
* Select select = new Select("select", selectionModel);
* add(select);
* SelectOptions options = new SelectOptions("options", elements, renderer);
* select.add(options);
* </pre>
*
* Note that you don't need to add component(s) for the <option> tag - they are created by
* SelectOptions
* </p>
* <p>
* <strong>Note</strong>: due to the usage of a SelectOption for each <option> the memory
* footprint of the page will grow with the number of <option>s you need. Consider using
* {@link org.apache.wicket.markup.html.form.DropDownChoice} component if it is able to fulfill your
* requirements.
* </p>
*
* @see SelectOption
* @see SelectOptions
* @see org.apache.wicket.markup.html.form.DropDownChoice
*
* @author Igor Vaynberg
* @param <T>
*/
public class Select<T> extends FormComponent<T>
{
private static final long serialVersionUID = 1L;
/**
* Constructor that will create a default model collection
*
* @param id
* component id
*/
public Select(final String id)
{
super(id);
}
/**
* @param id
* @param model
* @see WebMarkupContainer#WebMarkupContainer(String, IModel)
*/
public Select(final String id, final IModel<T> model)
{
super(id, model);
}
@Override
protected String getModelValue()
{
final StringBuilder builder = new StringBuilder();
visitChildren(SelectOption.class, new IVisitor<SelectOption<T>, Void>()
{
@Override
public void component(SelectOption<T> option, IVisit<Void> visit)
{
if (isSelected(option.getDefaultModel()))
{
if (builder.length() > 0)
{
builder.append(VALUE_SEPARATOR);
}
builder.append(option.getValue());
}
}
});
return builder.toString();
}
@Override
public void convertInput()
{
boolean supportsMultiple = getModelObject() instanceof Collection;
/*
* + * the input contains an array of values of the selected option components unless
* nothing was selected in which case the input contains null
*/
String[] values = getInputAsArray();
if ((values == null) || (values.length == 0))
{
setConvertedInput(null);
return;
}
if (!supportsMultiple && (values.length > 1))
{
throw new WicketRuntimeException(
"The model of Select component [" +
getPath() +
"] 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");
}
List<Object> converted = new ArrayList<>(values.length);
/*
* if the input is null we do not need to do anything since the model collection has already
* been cleared
*/
for (int i = 0; i < values.length; i++)
{
final String value = values[i];
if (!Strings.isEmpty(value))
{
SelectOption<T> option = visitChildren(SelectOption.class,
new IVisitor<SelectOption<T>, SelectOption<T>>()
{
@Override
public void component(SelectOption<T> option, IVisit<SelectOption<T>> visit)
{
if (String.valueOf(option.getValue()).equals(value))
{
visit.stop(option);
}
}
});
if (option == null)
{
throw new WicketRuntimeException(
"submitted http post value [" +
Arrays.toString(values) +
"] for SelectOption component [" +
getPath() +
"] contains an illegal value [" +
value +
"] 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.");
}
converted.add(option.getDefaultModelObject());
}
}
if (converted.isEmpty())
{
setConvertedInput(null);
}
else if (!supportsMultiple)
{
@SuppressWarnings("unchecked")
T convertedInput = (T)converted.get(0);
setConvertedInput(convertedInput);
}
else
{
@SuppressWarnings("unchecked")
T convertedInput = (T)converted;
setConvertedInput(convertedInput);
}
}
/**
* @see FormComponent#updateModel()
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void updateModel()
{
T object = getModelObject();
boolean supportsMultiple = object instanceof Collection;
Object converted = getConvertedInput();
/*
* update the model
*/
if (supportsMultiple)
{
Collection<?> modelCollection = (Collection<?>)object;
modelChanging();
modelCollection.clear();
if (converted != null)
{
modelCollection.addAll((Collection)converted);
}
modelChanged();
// force notify of model update via setObject()
setDefaultModelObject(modelCollection);
}
else
{
setDefaultModelObject(converted);
}
}
/**
* Checks if the specified option is selected based on raw input
*
* @param option
* @return {@code true} if the option is selected, {@code false} otherwise
*/
boolean isSelected(final SelectOption<?> option)
{
Args.notNull(option, "option");
// if the raw input is specified use that, otherwise use model
if (hasRawInput())
{
final String raw = getRawInput();
if (!Strings.isEmpty(raw))
{
String[] values = raw.split(VALUE_SEPARATOR);
for (int i = 0; i < values.length; i++)
{
String value = values[i];
if (value.equals(option.getValue()))
{
return true;
}
}
}
return false;
}
return isSelected(option.getDefaultModel());
}
/**
* Does the given model contain a selected value.
*
* @param model
* model to test on selection
* @return selected
*/
protected boolean isSelected(IModel<?> model)
{
return compareModels(getDefaultModelObject(), model.getObject());
}
private boolean compareModels(final Object selected, final Object value)
{
if ((selected != null) && (selected instanceof Collection))
{
if (value instanceof Collection)
{
return ((Collection<?>)selected).containsAll((Collection<?>)value);
}
else
{
return ((Collection<?>)selected).contains(value);
}
}
else
{
return Objects.equal(selected, value);
}
}
}