javax.faces.component.UISelectMany Maven / Gradle / Ivy
/*
* $Id: UISelectMany.java,v 1.61 2007/01/29 07:56:08 rlubke Exp $
*/
/*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* https://javaserverfaces.dev.java.net/CDDL.html or
* legal/CDDLv1.0.txt.
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at legal/CDDLv1.0.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* [Name of File] [ver.__] [Date]
*
* Copyright 2005 Sun Microsystems Inc. All Rights Reserved
*/
package javax.faces.component;
import java.lang.reflect.Array;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.model.SelectItem;
import javax.faces.model.SelectItemGroup;
/**
* UISelectMany is a {@link UIComponent} that represents
* the user's choice of a zero or more items from among a discrete set of
* available options. The user can modify the selected values. Optionally,
* the component can be preconfigured with zero or more currently selected
* items, by storing them as an array in the value
property of
* the component.
*
* This component is generally rendered as a select box or a group of
* checkboxes.
*
* By default, the rendererType
property must be set to
* "javax.faces.Listbox
". This value can be changed by
* calling the setRendererType()
method.
*
* The {@link javax.faces.render.Renderer} for this component must
* perform the following logic on getConvertedValue()
:
*
*
*
* Obtain the {@link javax.faces.convert.Converter} using the following algorithm:
*
*
*
* If the component has an attached {@link javax.faces.convert.Converter}, use it.
*
* If not, look for a {@link ValueExpression} for value
* (if any). The {@link ValueExpression} must point to something that
* is:
*
*
* - An array of primitives (such as
int[]
). Look up the
* registered by-class {@link javax.faces.convert.Converter} for this primitive type.
* - An array of objects (such as
Integer[]
or
* String[]
). Look up the registered by-class
* {@link javax.faces.convert.Converter} for the underlying element type.
* - A
java.util.List
. Assume that the element type is
* java.lang.String
, so no conversion is required.
*
*
* If for any reason a Converter
cannot be found, assume
* the type to be a String array.
*
*
*
* Use the selected {@link javax.faces.convert.Converter} (if any) to convert each element in the
* values array or list from the request to the proper type. If the component
* has a {@link ValueBinding} for value
, create an array
* of the expected type to hold the converted values. If the component
* does not have a {@link ValueBinding} for value
, create
* an array of type Object
. Store the created array
* as the local value of the component, set the component's valid
* state to true
and return.
*
*
*
*/
public class UISelectMany extends UIInput {
// ------------------------------------------------------ Manifest Constants
/**
* The standard component type for this component.
*/
public static final String COMPONENT_TYPE = "javax.faces.SelectMany";
/**
* The standard component family for this component.
*/
public static final String COMPONENT_FAMILY = "javax.faces.SelectMany";
/**
* The message identifier of the
* {@link javax.faces.application.FacesMessage} to be created if
* a value not matching the available options is specified.
*/
public static final String INVALID_MESSAGE_ID =
"javax.faces.component.UISelectMany.INVALID";
// ------------------------------------------------------------ Constructors
/**
*
Create a new {@link UISelectMany} instance with default property
* values.
*/
public UISelectMany() {
super();
setRendererType("javax.faces.Listbox");
}
// -------------------------------------------------------------- Properties
public String getFamily() {
return (COMPONENT_FAMILY);
}
/**
* Return the currently selected values, or null
if there
* are no currently selected values. This is a typesafe alias for
* getValue()
.
*/
public Object[] getSelectedValues() {
return ((Object[]) getValue());
}
/**
* Set the currently selected values, or null
to indicate
* that there are no currently selected values. This is a typesafe
* alias for setValue()
.
*
* @param selectedValues The new selected values (if any)
*/
public void setSelectedValues(Object selectedValues[]) {
setValue(selectedValues);
}
// ---------------------------------------------------------------- Bindings
/**
* Return any {@link ValueBinding} set for value
if
* a {@link ValueBinding} for selectedValues
is
* requested; otherwise, perform the default superclass processing
* for this method.
*
* This method relies on the superclass to provide the
* ValueExpression
to ValueBinding
* wrapping.
*
* @param name Name of the attribute or property for which to retrieve
* a {@link ValueBinding}
*
* @throws NullPointerException if name
* is null
*
* @deprecated this has been replaced by {@link #getValueExpression(java.lang.String)}.
*/
public ValueBinding getValueBinding(String name) {
if ("selectedValues".equals(name)) {
return (super.getValueBinding("value"));
} else {
return (super.getValueBinding(name));
}
}
/**
* Store any {@link ValueBinding} specified for
* selectedValues
under value
instead;
* otherwise, perform the default superclass processing for this
* method.
*
* This method relies on the superclass to wrap the argument
* ValueBinding
in a ValueExpression
.
*
* @param name Name of the attribute or property for which to set
* a {@link ValueBinding}
* @param binding The {@link ValueBinding} to set, or null
* to remove any currently set {@link ValueBinding}
*
* @throws NullPointerException if name
* is null
*
* @deprecated This has been replaced by {@link #setValueExpression(java.lang.String, javax.el.ValueExpression)}.
*/
public void setValueBinding(String name, ValueBinding binding) {
if ("selectedValues".equals(name)) {
super.setValueBinding("value", binding);
} else {
super.setValueBinding(name, binding);
}
}
/**
* Return any {@link ValueExpression} set for value
if a
* {@link ValueExpression} for selectedValues
is requested;
* otherwise, perform the default superclass processing for this method.
*
* @param name Name of the attribute or property for which to retrieve
* a {@link ValueExpression}
*
* @throws NullPointerException if name
* is null
* @since 1.2
*/
public ValueExpression getValueExpression(String name) {
if ("selectedValues".equals(name)) {
return (super.getValueExpression("value"));
} else {
return (super.getValueExpression(name));
}
}
/**
* Store any {@link ValueExpression} specified for
* selectedValues
under value
instead;
* otherwise, perform the default superclass processing for this method.
*
* @param name Name of the attribute or property for which to set
* a {@link ValueExpression}
* @param binding The {@link ValueExpression} to set, or null
* to remove any currently set {@link ValueExpression}
*
* @throws NullPointerException if name
* is null
* @since 1.2
*/
public void setValueExpression(String name, ValueExpression binding) {
if ("selectedValues".equals(name)) {
super.setValueExpression("value", binding);
} else {
super.setValueExpression(name, binding);
}
}
// --------------------------------------------------------- UIInput Methods
/**
* Return true
if the new value is different from the
* previous value. Value comparison must not be sensitive to element order.
*
*
* @param previous old value of this component
* @param value new value of this component
*/
protected boolean compareValues(Object previous, Object value) {
if ((previous == null) && (value != null)) {
return (true);
} else if ((previous != null) && (value == null)) {
return (true);
} else if ((previous == null)) {
return (false);
}
boolean valueChanged = false;
Object oldarray[];
Object newarray[];
// The arrays may be arrays of primitives; for simplicity,
// perform the boxing here.
if (!(previous instanceof Object[])) {
previous = toObjectArray(previous);
}
if (!(value instanceof Object[])) {
value = toObjectArray(value);
}
// If values are still not of the type Object[], it is perhaps a
// mistake by the renderers, so return false, so that
// ValueChangedEvent is not queued in this case.
if (!(previous instanceof Object[]) ||
!(value instanceof Object[])) {
return false;
}
oldarray = (Object[]) previous;
newarray = (Object[])value;
// If we got here then both the arrays cannot be null
// if their lengths vary, return false.
if ( oldarray.length != newarray.length) {
return true;
}
// make sure every element in the previous array occurs the same
// number of times in the current array. This should help us
// to find out the values changed are not. Since we cannot assume
// the browser will send the elements in the same order everytime,
// it will not suffice to just compare the element position and position.
int count1;
int count2;
for ( int i= 0; i < oldarray.length; ++i ) {
count1 = countElementOccurrence(oldarray[i], oldarray);
count2 = countElementOccurrence(oldarray[i], newarray);
if ( count1 != count2 ) {
valueChanged = true;
break;
}
}
return valueChanged;
}
/**
* Return the number of occurrances of a particular element in the
* array.
*
* @param element object whose occurrance is to be counted in the array.
* @param array object representing the old value of this component.
*/
private static int countElementOccurrence(Object element, Object[] array) {
int count = 0;
for ( int i= 0; i < array.length; ++i ) {
Object arrayElement = array[i];
if (arrayElement != null && element != null) {
if (arrayElement.equals(element)) {
count ++;
}
}
}
return count;
}
/**
* Convert an array of primitives to an array of boxed objects.
* @param primitiveArray object containing the primitive values
* @return an Object array, or null if the incoming value is not
* in fact an array at all.
*/
private static Object[] toObjectArray(Object primitiveArray) {
if (primitiveArray == null) {
throw new NullPointerException();
}
if (primitiveArray instanceof Object[]) {
return (Object[]) primitiveArray;
}
if (primitiveArray instanceof List) {
return ((List) primitiveArray).toArray();
}
Class clazz = primitiveArray.getClass();
if (!clazz.isArray()) {
return null;
}
int length = Array.getLength(primitiveArray);
Object[] array = new Object[length];
for (int i = 0; i < length; i++) {
array[i] = Array.get(primitiveArray, i);
}
return array;
}
// ------------------------------------------------------ Validation Methods
/**
* In addition to the standard validation behavior inherited from
* {@link UIInput}, ensure that any specified values are equal to one of
* the available options. Before comparing each option, coerce the
* option value type to the type of this component's value following
* the Expression Language coercion rules. If the specified value is not
* equal to any of the options, enqueue an error message
* and set the valid
property to false
.
*
* @param context The {@link FacesContext} for the current request
*
* @param value The converted value to test for membership.
*
* @throws NullPointerException if context
* is null
*/
protected void validateValue(FacesContext context, Object value) {
super.validateValue(context, value);
// Skip validation if it is not necessary
if (!isValid() || (value == null)) {
return;
}
// Ensure that the values match one of the available options
// Don't arrays cast to "Object[]", as we may now be using an array
// of primitives
boolean isList = (value instanceof List);
int length = isList ? ((List) value).size() : Array.getLength(value);
boolean found = true;
for (int i = 0; i < length; i++) {
Iterator items = new SelectItemsIterator(this);
Object indexValue = isList ?
((List) value).get(i) : Array.get(value, i);
found = matchValue(indexValue, items);
if (!found) {
break;
}
}
// Enqueue an error message if an invalid value was specified
if (!found) {
FacesMessage message =
MessageFactory.getMessage(context, INVALID_MESSAGE_ID,
MessageFactory.getLabel(context, this));
context.addMessage(getClientId(context), message);
setValid(false);
}
}
// --------------------------------------------------------- Private Methods
/**
* Return true
if the specified value matches one of the
* available options, performing a recursive search if if a
* {@link SelectItemGroup} instance is detected.
*
* @param value {@link UIComponent} value to be tested
* @param items Iterator over the {@link SelectItem}s to be checked
*/
private boolean matchValue(Object value, Iterator items) {
while (items.hasNext()) {
SelectItem item = (SelectItem) items.next();
if (item instanceof SelectItemGroup) {
SelectItem subitems[] =
((SelectItemGroup) item).getSelectItems();
if ((subitems != null) && (subitems.length > 0)) {
if (matchValue(value, new ArrayIterator(subitems))) {
return (true);
}
}
} else {
//Coerce the item value type before comparing values.
Class type = value.getClass();
Object newValue;
try {
newValue = getFacesContext().getApplication().
getExpressionFactory().coerceToType(item.getValue(), type);
} catch (Exception e) {
// this should catch an ELException, but there is a bug
// in ExpressionFactory.coerceToType() in GF
newValue = null;
}
if (value.equals(newValue)) {
return (true);
}
}
}
return (false);
}
static class ArrayIterator implements Iterator {
public ArrayIterator(Object items[]) {
this.items = items;
}
private Object items[];
private int index = 0;
public boolean hasNext() {
return (index < items.length);
}
public Object next() {
try {
return (items[index++]);
} catch (IndexOutOfBoundsException e) {
throw new NoSuchElementException();
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}