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 }