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