javax.faces.component.UISelectMany Maven / Gradle / Ivy
Show all versions of javax.faces Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package javax.faces.component;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.el.ValueBinding;
/**
* 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 or
* Collection
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.Collection
.
* Do not convert the values. Instead,
* convert the provided set of available options to string, exactly as done
* during render response, and for any match with the submitted values, add
* the available option as object to the collection.
*
*
*
* 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 from the request to the
* proper type, and store the result of
* each conversion in a data structure, called
* targetForConvertedValues for discussion. Create
* targetForConvertedValues using the following
* algorithm.
*
*
* If the component has a ValueExpression
for
* value
and the type of the expression is an array, let
* targetForConvertedValues be a new array of the expected
* type.
* If the component has a ValueExpression
for
* value
, let modelType be the type of the value
* expression. If modelType is a Collection
, do
* the following to arrive at targetForConvertedValues:
*
* Ask the component for its attribute under the key
* "collectionType
", without the quotes. If there is a
* value for that key, the value must be a String that is a fully
* qualified Java class name, or a Class
object, or a
* ValueExpression
that evaluates to a String or a
* Class
. In all cases, the value serves to identify the
* concrete type of the class that implements Collection
.
* For discussion, this is called collectionType. Let
* targetForConvertedValues be a new instance of
* Collection
implemented by the concrete class specified
* in collectionType. If, collectionType can not be
* discovered, or an instance of Collection
implemented by
* the concrete class specified in collectionType cannot be
* created, throw a {@link javax.faces.FacesException} with a correctly
* localized error message. Note that FacesException
is
* thrown instead of ConverterException
because this case
* would only arise from developer error, rather than end-user
* error.
* If there is no "collectionType
" attribute, call
* getValue()
on the component. The result will implement
* Collection
. If the result also implements
* Cloneable
, let targetForConvertedValues be the
* result of calling its clone()
method, then calling
* clear()
on the cloned Collection
. If
* unable to clone the value for any reason, log a message and proceed
* to the next step.
* If modelType is a concrete class, let
* targetForConvertedValues be a new instance of that class.
* Otherwise, the concrete type for targetForConvertedValues is
* taken from the following table. All classes are in the
* java.util
package. All collections must be created with
* an initial capacity equal to the length of the values array from the
* request.
*
* modelType to targetForConvertedValues mapping
*
* If modelType is an instance of
* then targetForConvertedValues must be an instance
* of
*
*
* SortedSet
* TreeSet
*
*
* Queue
* LinkedList
*
*
* Set
* HashSet
*
*
* anything else
* ArrayList
*
*
*
*
* If the component does not have a ValueExpression
* for value
, let targetForConvertedValues be an
* array of type Object
.
*
*
* Return targetForConvertedValues after populating it with
* the converted values.
*
*/
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
@Override
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()
.
*
* @return the selected values, or null
.
*/
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}
* @return the value binding, or null
* @throws NullPointerException if name
* is null
*
* @deprecated this has been replaced by {@link #getValueExpression(java.lang.String)}.
*/
@Override
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)}.
*/
@Override
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}
* @return the value expression, or null
.
* @throws NullPointerException if name
* is null
* @since 1.2
*/
@Override
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
*/
@Override
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
* @return true
if the new value is different from the
* previous value, false
otherwise.
*/
@Override
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 Collection) {
return ((Collection) 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
.
*
* This method must explicitly
* support a value argument that is a single value or a value
* argument that is a Collection
or Array of
* values.
* If {@link #isRequired} returns
* true
, and the current value is equal to the value of
* an inner {@link UISelectItem} whose {@link
* UISelectItem#isNoSelectionOption} method returns
* true
, 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
*/
@Override
protected void validateValue(FacesContext context, Object value) {
super.validateValue(context, value);
// Skip validation if it is not necessary
if (!isValid() || (value == null)) {
return;
}
boolean doAddMessage = false;
// 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
Converter converter = getConverter();
for (Iterator i = getValuesIterator(value); i.hasNext(); ) {
Iterator items = new SelectItemsIterator(context, this);
Object currentValue = i.next();
if (!SelectUtils.matchValue(context,
this,
currentValue,
items,
converter)) {
doAddMessage = true;
break;
}
}
// Ensure that if the value is noSelection and a
// value is required, a message is queued
if (isRequired()) {
for (Iterator i = getValuesIterator(value); i.hasNext();) {
Iterator items = new SelectItemsIterator(context, this);
Object currentValue = i.next();
if (SelectUtils.valueIsNoSelectionOption(context,
this,
currentValue,
items,
converter)) {
doAddMessage = true;
break;
}
}
}
if (doAddMessage) {
// Enqueue an error message if an invalid value was specified
FacesMessage message =
MessageFactory.getMessage(context,
INVALID_MESSAGE_ID,
MessageFactory.getLabel(context, this));
context.addMessage(getClientId(context), message);
setValid(false);
}
}
// --------------------------------------------------------- Private Methods
private Iterator getValuesIterator(Object value) {
if (value instanceof Collection) {
return ((Collection) value).iterator();
} else {
return (new ArrayIterator(value));
}
}
// ---------------------------------------------------------- Nested Classes
/**
* Exposes an Array as an Iterator.
*/
private static final class ArrayIterator implements Iterator {
private int length;
private int idx = 0;
private Object value;
// -------------------------------------------------------- Constructors
ArrayIterator(Object value) {
this.value = value;
length = Array.getLength(value);
}
// ------------------------------------------------------------ Iterator
@Override
public boolean hasNext() {
return (idx < length);
}
@Override
public Object next() {
if (idx >= length) {
throw new NoSuchElementException();
} else {
return Array.get(value, idx++);
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}