jakarta.faces.component.UISelectMany Maven / Gradle / Ivy
/*
* Copyright (c) 2022, 2022 Contributors to Eclipse Foundation.
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.faces.component;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import jakarta.el.ValueExpression;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
/**
*
* 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 "jakarta.faces.Listbox
". This value
* can be changed by calling the setRendererType()
method.
*
*
*
* The {@link jakarta.faces.render.Renderer} for this component must perform the following logic on
* getConvertedValue()
:
*
*
*
*
*
* Obtain the {@link jakarta.faces.convert.Converter} using the following algorithm:
*
*
*
*
*
* If the component has an attached {@link jakarta.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 jakarta.faces.convert.Converter} for this primitive type.
*
*
*
* -
*
* An array of objects (such as Integer[]
or String[]
). Look up the registered by-class
* {@link jakarta.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 jakarta.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 jakarta.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 = "jakarta.faces.SelectMany";
/**
*
* The standard component family for this component.
*
*/
public static final String COMPONENT_FAMILY = "jakarta.faces.SelectMany";
/**
*
* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if a value not matching
* the available options is specified.
*/
public static final String INVALID_MESSAGE_ID = "jakarta.faces.component.UISelectMany.INVALID";
// ------------------------------------------------------------ Constructors
/**
*
* Create a new {@link UISelectMany} instance with default property values.
*
*/
public UISelectMany() {
super();
setRendererType("jakarta.faces.Listbox");
}
// -------------------------------------------------------------- Properties
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
private transient Object submittedValue = null;
@Override
public Object getSubmittedValue() {
if (submittedValue == null && !isValid() && considerEmptyStringNull(FacesContext.getCurrentInstance())) { // JAVASERVERFACES_SPEC_PUBLIC-671
return new String[0]; // Mojarra#5081
}
return submittedValue;
}
@Override
public void setSubmittedValue(Object submittedValue) {
this.submittedValue = submittedValue;
}
/**
*
* 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 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 occurrences 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();
}
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();
}
}
}