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  package org.apache.myfaces.trinidad.component;
20  
21  import java.util.List;
22  import java.util.Map;
23  
24  import javax.faces.component.UIComponent;
25  import javax.faces.context.FacesContext;
26  import javax.faces.event.FacesEvent;
27  import javax.faces.event.PhaseId;
28  
29  import org.apache.myfaces.trinidad.event.DisclosureEvent;
30  import org.apache.myfaces.trinidad.event.FocusEvent;
31  import org.apache.myfaces.trinidad.event.RangeChangeEvent;
32  import org.apache.myfaces.trinidad.event.RowDisclosureEvent;
33  import org.apache.myfaces.trinidad.event.SelectionEvent;
34  import org.apache.myfaces.trinidad.event.SortEvent;
35  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
36  import org.apache.myfaces.trinidad.model.ModelUtils;
37  import org.apache.myfaces.trinidad.model.RowKeySet;
38  import org.apache.myfaces.trinidad.model.SortStrength;
39  import org.apache.myfaces.trinidad.model.TreeModel;
40  
41  /**
42   * utility methods for dealing with tables.
43   */
44  public final class TableUtils 
45  {
46  
47    /**
48     * Gets the index of the last visible row that should be
49     * displayed by the given table. This is usually 
50     * {@link CollectionComponent#getFirst} added to
51     * {@link CollectionComponent#getRows} minus 1, but it changes if 
52     * {@link CollectionComponent#getRowCount} returns
53     * insufficient rows.
54     * @return if this table is empty, this returns 
55     * {@link CollectionComponent#getFirst()} - 1
56     */
57    public static int getLast(CollectionComponent table)
58    {
59      return getLast(table, table.getFirst());
60    }
61  
62    /**
63     * Gets the index of the last visible row that should be
64     * displayed by the given table. This is usually 
65     * rangeStart added to
66     * {@link CollectionComponent#getRows} minus 1, but it changes if 
67     * {@link CollectionComponent#getRowCount} returns
68     * insufficient rows.
69     * @return if this table is empty, this returns 
70     * rangeStart - 1
71     */
72    public static int getLast(CollectionComponent table, int rangeStart)
73    {
74      final int rangeEnd;
75      int blockSize = table.getRows();
76      // if the blockSize is zero, that means show everthing.
77      if (blockSize <= 0)
78      {
79        rangeEnd = Integer.MAX_VALUE;
80      }
81      else
82      {
83        rangeEnd = rangeStart + blockSize;
84      }
85      return ModelUtils.findLastIndex(table, rangeStart, rangeEnd) - 1;
86    }
87  
88    /**
89     * Sets up an EL variable on the requestScope.
90     * @param name The name of the EL variable
91     * @param value The value of the EL variable
92     * @return any old value that was bound to the EL variable, or null
93     * if there was no old value.
94     */
95    @SuppressWarnings("unchecked")
96    public static Object setupELVariable(FacesContext context, String name, Object value)
97    {
98      Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
99      if (value == null)
100       return requestMap.remove(name);
101     else
102       return requestMap.put(name, value);
103   }
104 
105   /**
106    * Perform a safe expand all.
107    * Checks to make sure that a complete expand-all operation won't result in
108    * too many nodes being displayed to the user. If too many nodes will end
109    * up being displayed, this method only expands the immediate children before
110    * terminating. Otherwise, a complete expand-all operation is performed.
111    * @param maxSize the maximum number of nodes to display. A complete expand-all
112    * operation will not take place if more than this number of nodes 
113    * will end up being displayed.
114    * @param model this tree model must have its path pointing to a particular 
115    * node which must be a container.
116    */
117   static void __doSafeExpandAll(
118       TreeModel model, 
119       RowKeySet state, 
120       int       maxSize)
121   {
122     int size = _getSizeOfTree(model, maxSize);
123     if ((size < 0) || (size > maxSize))
124     {
125       // not safe to do expand all.
126       // first expand the current node:
127       state.add();
128       
129       // now only expand immediate children:
130       model.enterContainer();
131       int i=0;
132       while(true)
133       {
134         model.setRowIndex(i++);
135         if (!model.isRowAvailable())
136           break;
137         state.add();
138       }
139       model.exitContainer();
140     }
141     else // safe to do expand all:
142       state.addAll();
143   }
144 
145   /**
146    * Computes the number of nodes in a subtree.
147    * @param model the path must point to a node.
148    * @param maxSize the maximum number of nodes that will be searched.
149    * this method returns once this limit is reached.
150    * @return -1 if the number of nodes is unknown.
151    * If the limit is hit, then returns the number of nodes that are known
152    * to exist at the time the limit was hit. This number may be larger than
153    * maxSize.
154    */
155   static int _getSizeOfTree(TreeModel model, int maxSize)
156   {
157     if (model.isRowAvailable() && model.isContainer())
158     {
159       model.enterContainer();
160       try
161       {
162         int size = model.getRowCount();
163         for(int i=0, sz=size; i<sz; i++)
164         {
165           if (size > maxSize)
166             return size;
167           model.setRowIndex(i);
168           int kidSize = _getSizeOfTree(model, maxSize - size);
169           if (kidSize < 0)
170             return -1;
171           size += kidSize;
172         }
173         return size;        
174       }
175       finally
176       {
177         model.exitContainer();
178       }
179     }
180     return 0;
181   }
182 
183   /**
184    * This method sets the phaseID of the event
185    * according to the "immediate" property of this
186    * component.
187    * If "immediate" is set to true, this calls
188    * {@link FacesContext#renderResponse}
189    */
190   static void __handleQueueEvent(UIComponent comp, FacesEvent event)
191   {
192     if (_isImmediateEvent(comp, event))
193     {
194       String immediateAttr = UIXTree.IMMEDIATE_KEY.getName();
195       Object isImmediate = comp.getAttributes().get(immediateAttr);
196       if (Boolean.TRUE.equals(isImmediate))
197       {
198         event.setPhaseId(PhaseId.ANY_PHASE);
199         FacesContext context = FacesContext.getCurrentInstance();
200         context.renderResponse();
201       }
202       else
203       {
204         // the event should not execute before model updates are done. 
205         // otherwise, the updates will be done to the wrong rows.
206   
207         // we can't do this at the end of the UPDATE_MODEL phase because
208         // if there are errors during that phase, then we want to immediately render
209         // the response, and not deliver this ui event:
210         event.setPhaseId(PhaseId.INVOKE_APPLICATION);
211       }
212     }
213   }
214 
215   /**
216    * Process all the facets of a component; these are
217    * generally not processed once per row.
218    * @param skipFacet the name of any facet that should not be processed 
219    * at this time.
220    */
221   @SuppressWarnings("unchecked")
222   public static void processFacets(
223     FacesContext context,
224     final UIXCollection table,
225     UIComponent  component,
226     final PhaseId phaseId,
227     String skipFacet)
228   {
229     Map<String, UIComponent> facets = component.getFacets();
230     final UIComponent skip = (skipFacet != null)
231       ? (UIComponent) facets.get(skipFacet)
232       : null;
233                                            
234     new ChildLoop()
235     {
236       @Override
237       protected void process(FacesContext context, UIComponent facet, ComponentProcessingContext cpContext)
238       {
239         if (facet != skip)
240           table.processComponent(context, facet, phaseId);
241       }
242     }.runAlways(context, facets.values());
243   }
244 
245   /**
246    * Process all the facets of any children that are columns; these are
247    * generally not processed once per row.
248    */
249   public static void processColumnFacets(
250     FacesContext context,
251     final UIXCollection table,
252     UIComponent  column,
253     final PhaseId phaseId)
254   {
255     new ChildLoop()
256     {
257       @Override
258       protected void process(FacesContext context, UIComponent child, ComponentProcessingContext cpContext)
259       {
260         if (child instanceof UIXColumn && child.isRendered())
261         {
262           // process any facets of the child column:
263           processFacets(context, table, child, phaseId, null);
264           // recursively process the facets of any grandchild columns:
265           processColumnFacets(context, table, child, phaseId);
266         }
267       }
268     }.runAlways(context, column);
269   }
270 
271   /**
272    * Process all the children of the given table
273    */
274   public static void processStampedChildren(
275     FacesContext context,
276     final UIXCollection table,
277     final PhaseId phaseId)
278   {
279     new ChildLoop()
280     {
281       @Override
282       protected void process(FacesContext context, UIComponent child, ComponentProcessingContext cpContext)
283       {
284         // make sure that any cached clientIds are cleared so that
285         // the clientIds are recalculated with the new row index
286         UIXComponent.clearCachedClientIds(child);
287         table.processComponent(context, child, phaseId);
288       }
289     }.runAlways(context, table);
290   }
291   
292   /**
293    * Retrieves the sort strength for the column with the given sort property from the given table.
294    * @param parent the Collection object whose columns are searched for matching sortProperty and retrieving
295    * sort strength from.
296    * @param sortProperty sort property value to match against column's sortProperty property.
297    * @return sort strength for the column with the given sort property from the given table.
298    */
299   public static SortStrength findSortStrength(UIXCollection parent, String sortProperty)
300   {
301     SortStrength sortStrength = null;
302 
303     if (sortProperty == null || sortProperty.isEmpty())
304       return null;
305 
306     List<UIComponent> children = parent.getChildren();
307     for (UIComponent child : children)
308     {
309       if (child instanceof UIXColumn)
310       {
311         UIXColumn targetColumn = (UIXColumn)child;
312         if (sortProperty.equals(targetColumn.getSortProperty()))
313         {
314           String strength = targetColumn.getSortStrength();
315           sortStrength = _toSortStrength(strength);
316           break;
317         }
318       }
319     }
320 
321     return sortStrength;
322   }
323 
324   /**
325    * Convert the string value of sort strength to the SortStrength type.
326    */
327   private static SortStrength _toSortStrength(String strength)
328   {
329     SortStrength sortStrength = null;
330 
331     if (strength != null && !strength.isEmpty())
332     {
333       try
334       {
335         sortStrength = SortStrength.valueOf(strength.toUpperCase());
336       }
337       catch (IllegalArgumentException iae)
338       {
339          _LOG.warning("INVALID_SORT_STRENGTH_PROPERTY", strength);
340       }
341     }
342 
343     return sortStrength;
344   }
345 
346   /**
347    * Process all the children of the given table
348    */
349   @SuppressWarnings("unchecked")
350   static void __processChildren(
351     FacesContext context,
352     final UIXCollection comp,
353     final PhaseId phaseId)
354   {
355 
356     // process the children
357     int childCount = comp.getChildCount();
358     if (childCount != 0)
359     {
360       List<UIComponent> children = comp.getChildren();
361 
362       for (int i = 0; i < childCount; i++)
363       {
364         UIComponent child = children.get(i);
365         comp.processComponent(context, child, phaseId);
366       }
367     }          
368   }  
369 
370   static void cacheHeaderFooterFacets(UIComponent parent, Map<UIComponent, Boolean> cache)
371   {
372     // grab the header facet and it's children
373     UIComponent headerFacet = parent.getFacets().get("header");
374     if (headerFacet != null)
375     {
376       _cacheDescendants(headerFacet, cache, true);
377     }
378 
379     // grab the footer facet and it's children
380     UIComponent footerFacet = parent.getFacets().get("footer");
381     if (footerFacet != null)
382     {
383       _cacheDescendants(footerFacet, cache, true);
384     }
385   }
386 
387   static void cacheColumnHeaderFooterFacets(UIComponent parent, Map<UIComponent, Boolean> cache)
388   {
389     List<UIComponent> children = parent.getChildren();
390     for (UIComponent child : children)
391     {
392       if (child instanceof UIXColumn)
393       {
394         cacheHeaderFooterFacets(child, cache);
395         cacheColumnHeaderFooterFacets(child, cache);
396       }
397     }
398   }
399   
400 
401   private static void _cacheDescendants(UIComponent parent, Map<UIComponent, Boolean> cache, boolean inclusive)
402   {
403     if(inclusive)
404       cache.put(parent, Boolean.TRUE);
405     
406     List<UIComponent> children = parent.getChildren();
407     for (UIComponent child : children)
408     {
409       _cacheDescendants(child, cache, true);
410     }
411   }
412 
413   /**
414    * Checks to see if the given event could possible be affected by the 
415    * "immediate" property of the given component.
416    */
417   private static boolean _isImmediateEvent(UIComponent comp, FacesEvent event)
418   {
419     if (event.getComponent() == comp)
420     {
421       return 
422           (event instanceof RangeChangeEvent) ||
423           (event instanceof DisclosureEvent) ||
424           (event instanceof RowDisclosureEvent) ||
425           (event instanceof SelectionEvent) ||
426           (event instanceof SortEvent) ||
427           (event instanceof FocusEvent);
428     }
429     return false;
430   }
431 
432   private TableUtils()
433   {
434   }
435 
436   private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(TableUtils.class);
437 }