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.component.visit;
20  
21  import java.util.AbstractCollection;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.EnumSet;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.Map;
30  import java.util.Set;
31  
32  import javax.faces.component.NamingContainer;
33  import javax.faces.component.UIComponent;
34  import javax.faces.component.UINamingContainer;
35  import javax.faces.component.visit.VisitCallback;
36  import javax.faces.component.visit.VisitContext;
37  import javax.faces.component.visit.VisitHint;
38  import javax.faces.component.visit.VisitResult;
39  import javax.faces.context.FacesContext;
40  
41  /**
42   * <p>A VisitContext implementation that is
43   * used when performing a partial component tree visit.</p>
44   * 
45   * @author Werner Punz, Blake Sullivan (latest modification by $Author: lu4242 $)
46   * @version $Rev: 1296362 $ $Date: 2012-03-02 13:27:53 -0500 (Fri, 02 Mar 2012) $
47   */
48  public class PartialVisitContext extends VisitContext
49  {
50  
51    /**
52     * Creates a PartialVisitorContext instance.
53     * @param facesContext the FacesContext for the current request
54     * @param clientIds the client ids of the components to visit
55     * @throws NullPointerException  if {@code facesContext}
56     *                               is {@code null}
57     */    
58    public PartialVisitContext(
59      FacesContext facesContext,
60      Collection<String> clientIds)
61    {
62      this(facesContext, clientIds, null);
63    }
64  
65    /**
66     * Creates a PartialVisitorContext instance with the specified hints.
67     * @param facesContext the FacesContext for the current request
68     * @param clientIds the client ids of the components to visit
69     * @param hints a the VisitHints for this visit
70     * @throws NullPointerException  if {@code facesContext}
71     *                               is {@code null}
72     * @throws IllegalArgumentException if the phaseId is specified and
73     * hints does not contain VisitHint.EXECUTE_LIFECYCLE
74     */    
75    public PartialVisitContext(FacesContext facesContext,
76                               Collection<String> clientIds,
77                               Set<VisitHint> hints)
78    {
79      if (facesContext == null)
80      {
81          throw new NullPointerException();
82      }
83  
84      _facesContext = facesContext;
85  
86      // Copy the client ids into a HashSet to allow for quick lookups.
87  //    Set<String> clientIdSet = (clientIds == null)
88  //            ? new HashSet<String>()
89  //                    : new HashSet<String>(clientIds);
90  
91      // Initialize our various collections
92      // We maintain 4 collections:
93      //
94      // 1. clientIds: contains all of the client ids to visit
95      // 2. ids: contains just ids (not client ids) to visit.
96      //    We use this to optimize our check to see whether a
97      //    particular component is in the visit set (ie. to
98      //    avoid having to compute the client id).
99      // 3. subtreeClientIds: contains client ids to visit broken
100     //    out by naming container subtree.  (Needed by
101     //    getSubtreeIdsToVisit()).
102     // 4. unvisitedClientIds: contains the client ids to visit that
103     //    have not yet been visited.
104     //
105     // We populate these now.
106     //
107     // Note that we use default HashSet/Map initial capacities, though
108     // perhaps we could pick more intelligent defaults.
109 
110     // Initialize unvisitedClientIds collection
111     _unvisitedClientIds = new HashSet<String>();
112 
113     // Initialize ids collection
114     _ids = new HashSet<String>();
115 
116     // Intialize subtreeClientIds collection
117     _subtreeClientIds = new HashMap<String, Collection<String>>();
118 
119     // Initialize the clientIds collection.  Note that we proxy 
120     // this collection so that we can trap adds/removes and sync 
121     // up all of the other collections.
122     _clientIds = new CollectionProxy<String>(new HashSet<String>());
123 
124     // Finally, populate the clientIds collection.  This has the
125     // side effect of populating all of the other collections.
126     org.apache.myfaces.shared.util.ArrayUtils.addAll(_clientIds, clientIds);
127     //_clientIds.addAll(clientIdSet);
128 
129     // Copy and store hints - ensure unmodifiable and non-empty
130     EnumSet<VisitHint> hintsEnumSet = ((hints == null) || (hints.isEmpty()))
131                                         ? EnumSet.noneOf(VisitHint.class)
132                                         : EnumSet.copyOf(hints);
133 
134     _hints = Collections.unmodifiableSet(hintsEnumSet);
135   }
136 
137   /**
138    * @see VisitContext#getFacesContext VisitContext.getFacesContext()
139    */
140   @Override
141   public FacesContext getFacesContext()
142   {
143     return _facesContext;
144   }
145 
146   /**
147    * @see VisitContext#getHints VisitContext.getHints
148    */
149   @Override
150   public Set<VisitHint> getHints()
151   {
152     return _hints;
153   }
154 
155   /**
156    * @see VisitContext#getIdsToVisit VisitContext.getIdsToVisit()
157    */
158   @Override
159   public Collection<String> getIdsToVisit()
160   {
161     // We just return our clientIds collection.  This is
162     // the modifiable (but proxied) collection of all of
163     // the client ids to visit.
164     return _clientIds;
165   }
166 
167   /**
168    * @see VisitContext#getSubtreeIdsToVisit VisitContext.getSubtreeIdsToVisit()
169    */
170   @Override
171   public Collection<String> getSubtreeIdsToVisit(UIComponent component)
172   {
173     // Make sure component is a NamingContainer
174     if (!(component instanceof NamingContainer))
175     {
176       throw new IllegalArgumentException("Component is not a NamingContainer: " + component);
177     }
178 
179     String clientId = component.getClientId(getFacesContext());
180     Collection<String> ids = _subtreeClientIds.get(clientId);
181 
182     if (ids == null)
183     {
184         return Collections.emptyList();
185     }
186     else
187     {
188         return Collections.unmodifiableCollection(ids);
189     }
190   }
191 
192   /**
193    * @see VisitContext#invokeVisitCallback VisitContext.invokeVisitCallback()
194    */
195   @Override
196   public VisitResult invokeVisitCallback(
197     UIComponent component, 
198     VisitCallback callback)
199   {
200     // First sure that we should visit this component - ie.
201     // that this component is represented in our id set.
202     String clientId = _getVisitId(component);
203 
204     if (clientId == null)
205     {
206       // Not visiting this component, but allow visit to
207       // continue into this subtree in case we've got
208       // visit targets there.
209       return VisitResult.ACCEPT;
210     }
211 
212     // If we made it this far, the component matches one of
213     // client ids, so perform the visit.
214     VisitResult result = callback.visit(this, component);
215 
216     // Remove the component from our "unvisited" collection
217     _unvisitedClientIds.remove(clientId);
218 
219     // If the unvisited collection is now empty, we are done.
220     // Return VisitResult.COMPLETE to terminate the visit.
221     if (_unvisitedClientIds.isEmpty())
222     {
223         return VisitResult.COMPLETE;
224     }
225     else
226     {
227       // Otherwise, just return the callback's result 
228       return result;
229     }
230   }
231 
232 
233   // Called by CollectionProxy to notify PartialVisitContext that
234   // an new id has been added.
235   private void _idAdded(String clientId)
236   {
237     // An id to visit has been added, update our other
238     // collections to reflect this.
239 
240     // Update the ids collection
241     _ids.add(_getIdFromClientId(clientId));
242 
243     // Update the unvisited ids collection
244     _unvisitedClientIds.add(clientId);
245 
246     // Update the subtree ids collection
247     _addSubtreeClientId(clientId);
248   }
249 
250   // Called by CollectionProxy to notify PartialVisitContext that
251   // an id has been removed
252   private void _idRemoved(String clientId)
253   {
254     // An id to visit has been removed, update our other
255     // collections to reflect this.  Note that we don't
256     // update the ids collection, since we ids (non-client ids)
257     // may not be unique.
258 
259     // Update the unvisited ids collection
260     _unvisitedClientIds.remove(clientId);
261 
262     // Update the subtree ids collection
263     _removeSubtreeClientId(clientId);
264   }
265 
266   // Tests whether the specified component should be visited.
267   // If so, returns its client id.  If not, returns null.
268   private String _getVisitId(UIComponent component)
269   {
270     // We first check to see whether the component's id
271     // is in our id collection.  We do this before checking
272     // for the full client id because getting the full client id
273     // is more expensive than just getting the local id.
274     String id = component.getId();
275 
276     if ((id != null) && !_ids.contains(id))
277     {
278         return null;
279     }
280 
281       // The id was a match - now check the client id.
282     // note that client id should never be null (should be
283     // generated even if id is null, so asserting this.)
284     String clientId = component.getClientId(getFacesContext());
285     assert(clientId != null);
286 
287     return _clientIds.contains(clientId) ? clientId : null;
288   }
289 
290 
291 
292   // Converts an client id into a plain old id by ripping
293   // out the trailing id segmetn.
294   private String _getIdFromClientId(String clientId)
295   {
296     final char separator = UINamingContainer.getSeparatorChar(_facesContext);
297     int lastIndex = clientId.lastIndexOf(separator);
298 
299     String id = null;
300 
301     if (lastIndex < 0)
302     {
303       id = clientId;
304     }
305     else if (lastIndex < (clientId.length() - 1))
306     {
307       id = clientId.substring(lastIndex + 1);              
308     }
309     else
310     {
311       // TODO log warning for trailing colon case
312     }
313  
314     return id;
315   }
316 
317 
318   // Given a single client id, populate the subtree map with all possible
319   // subtree client ids
320   private void _addSubtreeClientId(String clientId)
321   {
322     // Loop over the client id and find the substring corresponding to
323     // each ancestor NamingContainer client id.  For each ancestor
324     // NamingContainer, add an entry into the map for the full client
325     // id.
326     final char separator = UINamingContainer.getSeparatorChar(_facesContext);
327     
328     int length = clientId.length();
329 
330     for (int i = 0; i < length; i++)
331     {
332       if (clientId.charAt(i) == separator)
333       {
334         // We found an ancestor NamingContainer client id - add 
335         // an entry to the map.
336         String namingContainerClientId = clientId.substring(0, i);
337 
338         // Check to see whether we've already ids under this
339         // NamingContainer client id.  If not, create the 
340         // Collection for this NamingContainer client id and
341         // stash it away in our map
342         Collection<String> c = _subtreeClientIds.get(namingContainerClientId);
343 
344         if (c == null)
345         {
346           // TODO: smarter initial size?
347           c = new ArrayList<String>();
348           _subtreeClientIds.put(namingContainerClientId, c);
349         }
350 
351         // Stash away the client id
352         c.add(clientId);
353       }
354     }
355   }
356 
357   // Given a single client id, remove any entries corresponding
358   // entries from our subtree collections
359   private void _removeSubtreeClientId(String clientId)
360   {
361     // Loop through each entry in the map and check to see whether
362     // the client id to remove should be contained in the corresponding
363     // collection - ie. whether the key (the NamingContainer client id)
364     // is present at the start of the client id to remove.
365     for (String key : _subtreeClientIds.keySet())
366     {
367       if (clientId.startsWith(key))
368       {
369         // If the clientId starts with the key, we should
370         // have an entry for this clientId in the corresponding
371         // collection.  Remove it.
372         Collection<String> ids = _subtreeClientIds.get(key);
373         ids.remove(clientId);
374       }
375     }
376   }
377 
378   // Little proxy collection implementation.  We proxy the id
379   // collection so that we can detect modifications and update
380   // our internal state when ids to visit are added or removed.
381   private class CollectionProxy<E extends String> extends AbstractCollection<E>
382   {
383     private CollectionProxy(Collection<E> wrapped)
384     {
385       _wrapped = wrapped;
386     }
387 
388     @Override
389     public int size()
390     {
391       return _wrapped.size();
392     }
393 
394     @Override
395     public Iterator<E> iterator()
396     {
397       return new IteratorProxy<E>(_wrapped.iterator());
398     }
399 
400     @Override
401     public boolean add(E o)
402     {
403       boolean added = _wrapped.add(o);
404 
405       if (added)
406       {
407         _idAdded(o);
408       }
409 
410       return added;
411     }
412 
413     private final Collection<E> _wrapped;
414   }
415 
416   // Little proxy iterator implementation used by CollectionProxy
417   // so that we can catch removes.
418   private class IteratorProxy<E extends String> implements Iterator<E>
419   {
420     private IteratorProxy(Iterator<E> wrapped)
421     {
422       _wrapped = wrapped;
423     }
424 
425     public boolean hasNext()
426     {
427       return _wrapped.hasNext();
428     }
429 
430     public E next()
431     {
432       _current = _wrapped.next();
433       
434       return _current;
435     }
436 
437     public void remove()
438     {
439       if (_current != null)
440       {
441         _idRemoved(_current);
442       }
443 
444       _wrapped.remove();
445     }
446 
447     private final Iterator<E> _wrapped;
448 
449     private E _current = null;
450   }
451 
452   // The client ids to visit
453   private final Collection<String> _clientIds;
454 
455   // The ids to visit
456   private final Collection<String> _ids;
457 
458   // The client ids that have yet to be visited
459   private final Collection<String> _unvisitedClientIds;
460 
461   // This map contains the information needed by getIdsToVisit().
462   // The keys in this map are NamingContainer client ids.  The values
463   // are collections containing all of the client ids to visit within
464   // corresponding naming container.
465   private final Map<String,Collection<String>> _subtreeClientIds;
466 
467   // The FacesContext for this request
468   private final FacesContext _facesContext;
469 
470   // Our visit hints
471   private final Set<VisitHint> _hints;
472 }