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: 1296363 $ $Date: 2012-03-02 13:28:11 -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 }