/* * 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; /** * <p><strong class="changed_modified_2_0 changed_modified_2_3">UISelectMany</strong> 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 <span class="changed_added_2_0">or * <code>Collection</code></span> in the <code>value</code> property of * the component.</p> * * <p>This component is generally rendered as a select box or a group of * checkboxes.</p> * * <p>By default, the <code>rendererType</code> property must be set to * "<code>javax.faces.Listbox</code>". This value can be changed by * calling the <code>setRendererType()</code> method.</p> * * <p>The {@link javax.faces.render.Renderer} for this component must * perform the following logic on <a * name="getConvertedValue"><code>getConvertedValue()</code></a>:</p> * * <blockquote> * * <p>Obtain the {@link javax.faces.convert.Converter} using the following algorithm:</p> * * <blockquote> * * <p>If the component has an attached {@link javax.faces.convert.Converter}, use it.</p> * * <p>If not, look for a {@link ValueExpression} for <code>value</code> * (if any). The {@link ValueExpression} must point to something that * is:</p> * * <ul> <li><p>An array of primitives (such as <code>int[]</code>). * Look up the registered by-class {@link javax.faces.convert.Converter} * for this primitive type.</p></li> * <li><p>An array of objects (such as <code>Integer[]</code> or * <code>String[]</code>). Look up the registered by-class {@link * javax.faces.convert.Converter} for the underlying element * type.</p></li> * <li class="changed_added_2_0"><p>A <code>java.util.Collection</code>. * Do not convert the values. <span class="changed_modified_2_3">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.</span></p></li> * </ul> * </blockquote> * * <p>If for any reason a <code>Converter</code> cannot be found, assume * the type to be a String array.</p> * </blockquote> * <p>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, <span class="changed_added_2_0">and store the result of * each conversion in a data structure, called * <em>targetForConvertedValues</em> for discussion. Create * <em>targetForConvertedValues</em> using the following * algorithm.</span></p> * <div class="changed_added_2_0"> * <ul> * <li><p>If the component has a <code>ValueExpression</code> for * <code>value</code> and the type of the expression is an array, let * <em>targetForConvertedValues</em> be a new array of the expected * type.</p></li> * <li><p>If the component has a <code>ValueExpression</code> for * <code>value</code>, let <em>modelType</em> be the type of the value * expression. If <em>modelType</em> is a <code>Collection</code>, do * the following to arrive at <em>targetForConvertedValues</em>:</p> * <ul> * <li><p>Ask the component for its attribute under the key * "<code>collectionType</code>", 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 <code>Class</code> object, or a * <code>ValueExpression</code> that evaluates to a String or a * <code>Class</code>. In all cases, the value serves to identify the * concrete type of the class that implements <code>Collection</code>. * For discussion, this is called <em>collectionType</em>. Let * <em>targetForConvertedValues</em> be a new instance of * <code>Collection</code> implemented by the concrete class specified * in <em>collectionType</em>. If, <em>collectionType</em> can not be * discovered, or an instance of <code>Collection</code> implemented by * the concrete class specified in <em>collectionType</em> cannot be * created, throw a {@link javax.faces.FacesException} with a correctly * localized error message. Note that <code>FacesException</code> is * thrown instead of <code>ConverterException</code> because this case * would only arise from developer error, rather than end-user * error.</p></li> * <li><p>If there is no "<code>collectionType</code>" attribute, call * <code>getValue()</code> on the component. The result will implement * <code>Collection</code>. If the result also implements * <code>Cloneable</code>, let <em>targetForConvertedValues</em> be the * result of calling its <code>clone()</code> method, then calling * <code>clear()</code> on the cloned <code>Collection</code>. If * unable to clone the value for any reason, log a message and proceed * to the next step.</p></li> * <li><p>If <em>modelType</em> is a concrete class, let * <em>targetForConvertedValues</em> be a new instance of that class. * Otherwise, the concrete type for <em>targetForConvertedValues</em> is * taken from the following table. All classes are in the * <code>java.util</code> package. All collections must be created with * an initial capacity equal to the length of the values array from the * request.</p> * <table border="1"> * <caption>modelType to targetForConvertedValues mapping</caption> * <tr> * <th>If <em>modelType</em> is an instance of</th> * <th>then <em>targetForConvertedValues</em> must be an instance * of</th> * </tr> * <tr> * <td><code>SortedSet</code></td> * <td><code>TreeSet</code></td> * </tr> * <tr> * <td><code>Queue</code></td> * <td><code>LinkedList</code></td> * </tr> * <tr> * <td><code>Set</code></td> * <td><code>HashSet</code></td> * </tr> * <tr> * <td>anything else</td> * <td><code>ArrayList</code></td> * </tr> * </table> * </li> * </ul> * <li><p>If the component does not have a <code>ValueExpression</code> * for <code>value</code>, let <em>targetForConvertedValues</em> be an * array of type <code>Object</code>.</p> * </ul> * </div> * <p>Return <em>targetForConvertedValues</em> after populating it with * the converted values.</p> * */ public class UISelectMany extends UIInput { // ------------------------------------------------------ Manifest Constants /** * <p>The standard component type for this component.</p> */ public static final String COMPONENT_TYPE = "javax.faces.SelectMany"; /** * <p>The standard component family for this component.</p> */ public static final String COMPONENT_FAMILY = "javax.faces.SelectMany"; /** * <p>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 /** * <p>Create a new {@link UISelectMany} instance with default property * values.</p> */ public UISelectMany() { super(); setRendererType("javax.faces.Listbox"); } // -------------------------------------------------------------- Properties @Override public String getFamily() { return (COMPONENT_FAMILY); } /** * <p>Return the currently selected values, or <code>null</code> if there * are no currently selected values. This is a typesafe alias for * <code>getValue()</code>.</p> * * @return the selected values, or <code>null</code>. */ public Object[] getSelectedValues() { return ((Object[]) getValue()); } /** * <p>Set the currently selected values, or <code>null</code> to indicate * that there are no currently selected values. This is a typesafe * alias for <code>setValue()</code>.</p> * * @param selectedValues The new selected values (if any) */ public void setSelectedValues(Object selectedValues[]) { setValue(selectedValues); } // ---------------------------------------------------------------- Bindings /** * <p>Return any {@link ValueBinding} set for <code>value</code> if * a {@link ValueBinding} for <code>selectedValues</code> is * requested; otherwise, perform the default superclass processing * for this method.</p> * * <p>This method relies on the superclass to provide the * <code>ValueExpression</code> to <code>ValueBinding</code> * wrapping.</p> * * @param name Name of the attribute or property for which to retrieve * a {@link ValueBinding} * @return the value binding, or <code>null</code> * @throws NullPointerException if <code>name</code> * is <code>null</code> * * @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)); } } /** * <p>Store any {@link ValueBinding} specified for * <code>selectedValues</code> under <code>value</code> instead; * otherwise, perform the default superclass processing for this * method.</p> * * <p>This method relies on the superclass to wrap the argument * <code>ValueBinding</code> in a <code>ValueExpression</code>.</p> * * @param name Name of the attribute or property for which to set * a {@link ValueBinding} * @param binding The {@link ValueBinding} to set, or <code>null</code> * to remove any currently set {@link ValueBinding} * * @throws NullPointerException if <code>name</code> * is <code>null</code> * * @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); } } /** * <p>Return any {@link ValueExpression} set for <code>value</code> if a * {@link ValueExpression} for <code>selectedValues</code> is requested; * otherwise, perform the default superclass processing for this method.</p> * * @param name Name of the attribute or property for which to retrieve * a {@link ValueExpression} * @return the value expression, or <code>null</code>. * @throws NullPointerException if <code>name</code> * is <code>null</code> * @since 1.2 */ @Override public ValueExpression getValueExpression(String name) { if ("selectedValues".equals(name)) { return (super.getValueExpression("value")); } else { return (super.getValueExpression(name)); } } /** * <p>Store any {@link ValueExpression} specified for * <code>selectedValues</code> under <code>value</code> instead; * otherwise, perform the default superclass processing for this method.</p> * * @param name Name of the attribute or property for which to set * a {@link ValueExpression} * @param binding The {@link ValueExpression} to set, or <code>null</code> * to remove any currently set {@link ValueExpression} * * @throws NullPointerException if <code>name</code> * is <code>null</code> * @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 /** * <p>Return <code>true</code> if the new value is different from the * previous value. Value comparison must not be sensitive to element order. * </p> * * @param previous old value of this component * @param value new value of this component * @return <code>true</code> if the new value is different from the * previous value, <code>false</code> 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; } /** * <p>Return the number of occurrances of a particular element in the * array.</p> * * @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 /** * <p><span class="changed_modified_2_0">In</span> 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 * <code>valid</code> property to <code>false</code>.</p> * * <p class="changed_modified_2_0">This method must explicitly * support a value argument that is a single value or a value * argument that is a <code>Collection</code> or Array of * values.</p> * <p class="changed_added_2_0">If {@link #isRequired} returns * <code>true</code>, and the current value is equal to the value of * an inner {@link UISelectItem} whose {@link * UISelectItem#isNoSelectionOption} method returns * <code>true</code>, enqueue an error message and set the * <code>valid</code> property to <code>false</code>.</p> * @param context The {@link FacesContext} for the current request * * @param value The converted value to test for membership. * * @throws NullPointerException if <code>context</code> * is <code>null</code> */ @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(); } } }