View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.util;
21  
22  import org.apache.myfaces.tobago.component.Attributes;
23  import org.apache.myfaces.tobago.component.Visual;
24  import org.apache.myfaces.tobago.context.Markup;
25  import org.apache.myfaces.tobago.util.ComponentUtils;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import javax.el.ValueExpression;
30  import javax.faces.component.UIComponent;
31  import javax.faces.component.UISelectItem;
32  import javax.faces.component.UISelectItems;
33  import javax.faces.context.FacesContext;
34  import javax.faces.model.SelectItem;
35  import java.lang.invoke.MethodHandles;
36  import java.lang.reflect.Array;
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.Iterator;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.NoSuchElementException;
44  
45  /**
46   * Based on code from MyFaces core.
47   */
48  public class SelectItemUtils {
49  
50    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
51  
52    /**
53     * Creates a list of SelectItems to use for rendering.
54     */
55    public static Iterable<SelectItem> getItemIterator(final FacesContext facesContext, final UIComponent selector) {
56      if (selector.getChildCount() == 0) {
57        return Collections.emptyList();
58      } else {
59        return new Iterable<SelectItem>() {
60  
61          private SelectItemsIterator iterator;
62  
63          @Override
64          public Iterator<SelectItem> iterator() {
65            if (iterator == null) {
66              iterator = new SelectItemsIterator(facesContext, selector);
67            }
68            return iterator;
69          }
70        };
71      }
72    }
73  
74    /**
75     * Creates a list of SelectItems to use for rendering.
76     * You should only use this method (which returns a list), when you need a list.
77     * Otherwise please use {@link #getItemIterator(javax.faces.context.FacesContext, javax.faces.component.UIComponent)}
78     */
79    public static List<SelectItem> getItemList(final FacesContext facesContext, final UIComponent selector) {
80      if (selector.getChildCount() == 0) {
81        return Collections.emptyList();
82      } else {
83        final Iterable<SelectItem> iterator = getItemIterator(facesContext, selector);
84        final List<SelectItem> result = new ArrayList<>();
85        for (final SelectItem selectItem : iterator) {
86          result.add(selectItem);
87        }
88        return result;
89      }
90    }
91  
92    private static class SelectItemsIterator implements Iterator<SelectItem> {
93  
94      private final FacesContext facesContext;
95      private final Iterator<UIComponent> children;
96      private Iterator<?> nestedItems;
97      private SelectItem nextItem;
98      private UISelectItems currentUISelectItems;
99  
100     private SelectItemsIterator(final FacesContext facesContext, final UIComponent selector) {
101       this.children = selector.getChildren().iterator();
102       this.facesContext = facesContext;
103     }
104 
105     @Override
106     @SuppressWarnings("unchecked")
107     public boolean hasNext() {
108       if (nextItem != null) {
109         return true;
110       }
111       if (nestedItems != null) {
112         if (nestedItems.hasNext()) {
113           return true;
114         }
115         nestedItems = null;
116       }
117 
118       UIComponent child = null;
119       while (children.hasNext()) {
120         final UIComponent c = children.next();
121         // When there is other components nested that does
122         // not extends from UISelectItem or UISelectItems
123         // the behavior for this iterator is just skip this
124         // element(s) until an element that extends from these
125         // classes are found. If there is no more elements
126         // that conform this condition, just return false.
127         if (c instanceof UISelectItem || c instanceof UISelectItems) {
128           child = c;
129           break;
130         }
131       }
132       if (child == null) {
133         return false;
134       }
135 
136       if (child instanceof UISelectItem) {
137         final UISelectItem uiSelectItem = (UISelectItem) child;
138         Object item = uiSelectItem.getValue();
139         if (item == null) {
140           // no value attribute --> create the SelectItem out of the other attributes
141           final Object itemValue = uiSelectItem.getItemValue();
142           String label = uiSelectItem.getItemLabel();
143           final String description = uiSelectItem.getItemDescription();
144           final boolean disabled = uiSelectItem.isItemDisabled();
145 //          boolean escape = uiSelectItem.isItemEscaped();
146 //          boolean noSelectionOption = uiSelectItem.isNoSelectionOption();
147           if (label == null && itemValue != null) {
148             label = itemValue.toString();
149           }
150           String image = null;
151           Markup markup = null;
152           if (uiSelectItem instanceof org.apache.myfaces.tobago.component.UISelectItem) {
153             final org.apache.myfaces.tobago.component.UISelectItem tobagoSelectItem
154                 = (org.apache.myfaces.tobago.component.UISelectItem) uiSelectItem;
155             image = tobagoSelectItem.getItemImage();
156             markup = tobagoSelectItem.getMarkup();
157           }
158           item = new org.apache.myfaces.tobago.model.SelectItem(itemValue, label, description, disabled, image, markup);
159         } else if (!(item instanceof SelectItem)) {
160           final ValueExpression expression = uiSelectItem.getValueExpression("value");
161           throw new IllegalArgumentException("ValueExpression '"
162               + (expression == null ? null : expression.getExpressionString()) + "' of UISelectItem : "
163               + child + " does not reference an Object of type SelectItem");
164         }
165         nextItem = (SelectItem) item;
166         return true;
167       } else { // UISelectItems
168         currentUISelectItems = (UISelectItems) child;
169         final Object value = currentUISelectItems.getValue();
170 
171         if (value instanceof SelectItem) {
172           nextItem = (SelectItem) value;
173           return true;
174         } else if (value != null && value.getClass().isArray()) {
175           // value is any kind of array (primitive or non-primitive)
176           // --> we have to use class Array to get the values
177           final int length = Array.getLength(value);
178           final Collection<Object> items = new ArrayList<>(length);
179           for (int i = 0; i < length; i++) {
180             items.add(Array.get(value, i));
181           }
182           nestedItems = items.iterator();
183           return hasNext();
184         } else if (value instanceof Iterable) {
185           // value is Iterable --> Collection, DataModel,...
186           nestedItems = ((Iterable<?>) value).iterator();
187           return hasNext();
188         } else if (value instanceof Map) {
189           final Map<Object, Object> map = (Map<Object, Object>) value;
190           final Collection<SelectItem> items = new ArrayList<>(map.size());
191           for (final Map.Entry<Object, Object> entry : map.entrySet()) {
192             items.add(new org.apache.myfaces.tobago.model.SelectItem(entry.getValue(), entry.getKey().toString()));
193           }
194           nestedItems = items.iterator();
195           return hasNext();
196         }
197       }
198       return false;
199     }
200 
201     @Override
202     public SelectItem next() {
203       if (!hasNext()) {
204         throw new NoSuchElementException();
205       }
206       if (nextItem != null) {
207         final SelectItem value = nextItem;
208         nextItem = null;
209         return value;
210       }
211       if (nestedItems != null) {
212         Object item = nestedItems.next();
213 
214         if (!(item instanceof SelectItem)) {
215           // check new params of SelectItems (since 2.0): itemValue, itemLabel, itemDescription,...
216           // Note that according to the spec UISelectItems does not provide Getter and Setter
217           // methods for this values, so we have to use the attribute map
218           final Map<String, Object> attributeMap = currentUISelectItems.getAttributes();
219 
220           // write the current item into the request map under the key listed in var, if available
221           boolean wroteRequestMapVarValue = false;
222           Object oldRequestMapVarValue = null;
223           final String var = ComponentUtils.getStringAttribute(currentUISelectItems, Attributes.var);
224           if (var != null && !"".equals(var)) {
225             // save the current value of the key listed in var from the request map
226             oldRequestMapVarValue = facesContext.getExternalContext().getRequestMap().put(var, item);
227             wroteRequestMapVarValue = true;
228           }
229 
230           // check the itemValue attribute
231           Object itemValue = ComponentUtils.getAttribute(currentUISelectItems, Attributes.itemValue);
232           if (itemValue == null) {
233             // the itemValue attribute was not provided
234             // --> use the current item as the itemValue
235             itemValue = item;
236           }
237 
238           // Spec: When iterating over the select items, toString()
239           // must be called on the string rendered attribute values
240           final Object itemLabelObject = ComponentUtils.getAttribute(currentUISelectItems, Attributes.itemLabel);
241           final String itemLabel;
242           if (itemLabelObject != null) {
243             itemLabel = itemLabelObject.toString();
244           } else if (itemValue != null) {
245             itemLabel = itemValue.toString();
246           } else {
247             LOG.warn("Label string can't be created!");
248             itemLabel = "???";
249           }
250           Object itemDescription = ComponentUtils.getAttribute(currentUISelectItems, Attributes.itemDescription);
251           if (itemDescription != null) {
252             itemDescription = itemDescription.toString();
253           }
254           final boolean itemDisabled
255               = ComponentUtils.getBooleanAttribute(currentUISelectItems, Attributes.itemDisabled, false);
256           final String itemImage = ComponentUtils.getStringAttribute(currentUISelectItems, Attributes.itemImage);
257           final Markup markup;
258           if (currentUISelectItems instanceof Visual) {
259             markup = ((Visual) currentUISelectItems).getMarkup();
260           } else {
261             markup = Markup.NULL;
262           }
263 // TBD: should this be possible?
264 //        Boolean itemLabelEscaped = getBooleanAttribute(currentUISelectItems, ITEM_LABEL_ESCAPED_PROP, true);
265 // TBD ?
266 //        Object noSelectionValue = attributeMap.get(NO_SELECTION_VALUE_PROP);
267           item = new org.apache.myfaces.tobago.model.SelectItem(
268               itemValue, itemLabel, (String) itemDescription, itemDisabled, itemImage, markup);
269 
270           // remove the value with the key from var from the request map, if previously written
271           if (wroteRequestMapVarValue) {
272             // If there was a previous value stored with the key from var in the request map, restore it
273             if (oldRequestMapVarValue != null) {
274               facesContext.getExternalContext().getRequestMap().put(var, oldRequestMapVarValue);
275             } else {
276               facesContext.getExternalContext().getRequestMap().remove(var);
277             }
278           }
279         }
280         return (SelectItem) item;
281       }
282       throw new NoSuchElementException();
283     }
284 
285     @Override
286     public void remove() {
287       throw new UnsupportedOperationException();
288     }
289   }
290 
291 }