/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
package org.apache.myfaces.test.mock.visit; | |
import java.util.AbstractCollection; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.EnumSet; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Iterator; | |
import java.util.Map; | |
import java.util.Set; | |
import javax.faces.component.NamingContainer; | |
import javax.faces.component.UIComponent; | |
import javax.faces.component.UINamingContainer; | |
import javax.faces.component.visit.VisitCallback; | |
import javax.faces.component.visit.VisitContext; | |
import javax.faces.component.visit.VisitHint; | |
import javax.faces.component.visit.VisitResult; | |
import javax.faces.context.FacesContext; | |
/** | |
* <p>A VisitContext implementation that is | |
* used when performing a partial component tree visit.</p> | |
* | |
* @author Werner Punz, Blake Sullivan (latest modification by $Author: lu4242 $) | |
* @version $Rev: 949094 $ $Date: 2010-05-27 22:59:10 -0500 (Jue, 27 May 2010) $ | |
* @since 1.0.0 | |
*/ | |
public class PartialVisitContext extends VisitContext | |
{ | |
/** | |
* Creates a PartialVisitorContext instance. | |
* @param facesContext the FacesContext for the current request | |
* @param clientIds the client ids of the components to visit | |
* @throws NullPointerException if {@code facesContext} | |
* is {@code null} | |
*/ | |
public PartialVisitContext(FacesContext facesContext, | |
Collection<String> clientIds) | |
{ | |
this(facesContext, clientIds, null); | |
} | |
/** | |
* Creates a PartialVisitorContext instance with the specified hints. | |
* @param facesContext the FacesContext for the current request | |
* @param clientIds the client ids of the components to visit | |
* @param hints a the VisitHints for this visit | |
* @throws NullPointerException if {@code facesContext} | |
* is {@code null} | |
* @throws IllegalArgumentException if the phaseId is specified and | |
* hints does not contain VisitHint.EXECUTE_LIFECYCLE | |
*/ | |
public PartialVisitContext(FacesContext facesContext, | |
Collection<String> clientIds, Set<VisitHint> hints) | |
{ | |
if (facesContext == null) | |
{ | |
throw new NullPointerException(); | |
} | |
_facesContext = facesContext; | |
// Copy the client ids into a HashSet to allow for quick lookups. | |
Set<String> clientIdSet = (clientIds == null) ? new HashSet<String>() | |
: new HashSet<String>(clientIds); | |
// Initialize our various collections | |
// We maintain 4 collections: | |
// | |
// 1. clientIds: contains all of the client ids to visit | |
// 2. ids: contains just ids (not client ids) to visit. | |
// We use this to optimize our check to see whether a | |
// particular component is in the visit set (ie. to | |
// avoid having to compute the client id). | |
// 3. subtreeClientIds: contains client ids to visit broken | |
// out by naming container subtree. (Needed by | |
// getSubtreeIdsToVisit()). | |
// 4. unvisitedClientIds: contains the client ids to visit that | |
// have not yet been visited. | |
// | |
// We populate these now. | |
// | |
// Note that we use default HashSet/Map initial capacities, though | |
// perhaps we could pick more intelligent defaults. | |
// Initialize unvisitedClientIds collection | |
_unvisitedClientIds = new HashSet<String>(); | |
// Initialize ids collection | |
_ids = new HashSet<String>(); | |
// Intialize subtreeClientIds collection | |
_subtreeClientIds = new HashMap<String, Collection<String>>(); | |
// Initialize the clientIds collection. Note that we proxy | |
// this collection so that we can trap adds/removes and sync | |
// up all of the other collections. | |
_clientIds = new CollectionProxy<String>(new HashSet<String>()); | |
// Finally, populate the clientIds collection. This has the | |
// side effect of populating all of the other collections. | |
_clientIds.addAll(clientIdSet); | |
// Copy and store hints - ensure unmodifiable and non-empty | |
EnumSet<VisitHint> hintsEnumSet = ((hints == null) || (hints.isEmpty())) ? EnumSet | |
.noneOf(VisitHint.class) | |
: EnumSet.copyOf(hints); | |
_hints = Collections.unmodifiableSet(hintsEnumSet); | |
} | |
/** | |
* @see VisitContext#getFacesContext VisitContext.getFacesContext() | |
*/ | |
@Override | |
public FacesContext getFacesContext() | |
{ | |
return _facesContext; | |
} | |
/** | |
* @see VisitContext#getHints VisitContext.getHints | |
*/ | |
@Override | |
public Set<VisitHint> getHints() | |
{ | |
return _hints; | |
} | |
/** | |
* @see VisitContext#getIdsToVisit VisitContext.getIdsToVisit() | |
*/ | |
@Override | |
public Collection<String> getIdsToVisit() | |
{ | |
// We just return our clientIds collection. This is | |
// the modifiable (but proxied) collection of all of | |
// the client ids to visit. | |
return _clientIds; | |
} | |
/** | |
* @see VisitContext#getSubtreeIdsToVisit VisitContext.getSubtreeIdsToVisit() | |
*/ | |
@Override | |
public Collection<String> getSubtreeIdsToVisit(UIComponent component) | |
{ | |
// Make sure component is a NamingContainer | |
if (!(component instanceof NamingContainer)) | |
{ | |
throw new IllegalArgumentException( | |
"Component is not a NamingContainer: " + component); | |
} | |
String clientId = component.getClientId(getFacesContext()); | |
Collection<String> ids = _subtreeClientIds.get(clientId); | |
if (ids == null) | |
{ | |
return Collections.emptyList(); | |
} | |
else | |
{ | |
return Collections.unmodifiableCollection(ids); | |
} | |
} | |
/** | |
* @see VisitContext#invokeVisitCallback VisitContext.invokeVisitCallback() | |
*/ | |
@Override | |
public VisitResult invokeVisitCallback(UIComponent component, | |
VisitCallback callback) | |
{ | |
// First sure that we should visit this component - ie. | |
// that this component is represented in our id set. | |
String clientId = _getVisitId(component); | |
if (clientId == null) | |
{ | |
// Not visiting this component, but allow visit to | |
// continue into this subtree in case we've got | |
// visit targets there. | |
return VisitResult.ACCEPT; | |
} | |
// If we made it this far, the component matches one of | |
// client ids, so perform the visit. | |
VisitResult result = callback.visit(this, component); | |
// Remove the component from our "unvisited" collection | |
_unvisitedClientIds.remove(clientId); | |
// If the unvisited collection is now empty, we are done. | |
// Return VisitResult.COMPLETE to terminate the visit. | |
if (_unvisitedClientIds.isEmpty()) | |
{ | |
return VisitResult.COMPLETE; | |
} | |
else | |
{ | |
// Otherwise, just return the callback's result | |
return result; | |
} | |
} | |
// Called by CollectionProxy to notify PartialVisitContext that | |
// an new id has been added. | |
private void _idAdded(String clientId) | |
{ | |
// An id to visit has been added, update our other | |
// collections to reflect this. | |
// Update the ids collection | |
_ids.add(_getIdFromClientId(clientId)); | |
// Update the unvisited ids collection | |
_unvisitedClientIds.add(clientId); | |
// Update the subtree ids collection | |
_addSubtreeClientId(clientId); | |
} | |
// Called by CollectionProxy to notify PartialVisitContext that | |
// an id has been removed | |
private void _idRemoved(String clientId) | |
{ | |
// An id to visit has been removed, update our other | |
// collections to reflect this. Note that we don't | |
// update the ids collection, since we ids (non-client ids) | |
// may not be unique. | |
// Update the unvisited ids collection | |
_unvisitedClientIds.remove(clientId); | |
// Update the subtree ids collection | |
_removeSubtreeClientId(clientId); | |
} | |
// Tests whether the specified component should be visited. | |
// If so, returns its client id. If not, returns null. | |
private String _getVisitId(UIComponent component) | |
{ | |
// We first check to see whether the component's id | |
// is in our id collection. We do this before checking | |
// for the full client id because getting the full client id | |
// is more expensive than just getting the local id. | |
String id = component.getId(); | |
if ((id != null) && !_ids.contains(id)) | |
{ | |
return null; | |
} | |
// The id was a match - now check the client id. | |
// note that client id should never be null (should be | |
// generated even if id is null, so asserting this.) | |
String clientId = component.getClientId(getFacesContext()); | |
assert (clientId != null); | |
return _clientIds.contains(clientId) ? clientId : null; | |
} | |
// Converts an client id into a plain old id by ripping | |
// out the trailing id segmetn. | |
private String _getIdFromClientId(String clientId) | |
{ | |
final char separator = UINamingContainer | |
.getSeparatorChar(_facesContext); | |
int lastIndex = clientId.lastIndexOf(separator); | |
String id = null; | |
if (lastIndex < 0) | |
{ | |
id = clientId; | |
} | |
else if (lastIndex < (clientId.length() - 1)) | |
{ | |
id = clientId.substring(lastIndex + 1); | |
} | |
else | |
{ | |
// TODO log warning for trailing colon case | |
} | |
return id; | |
} | |
// Given a single client id, populate the subtree map with all possible | |
// subtree client ids | |
private void _addSubtreeClientId(String clientId) | |
{ | |
// Loop over the client id and find the substring corresponding to | |
// each ancestor NamingContainer client id. For each ancestor | |
// NamingContainer, add an entry into the map for the full client | |
// id. | |
final char separator = UINamingContainer | |
.getSeparatorChar(_facesContext); | |
int length = clientId.length(); | |
for (int i = 0; i < length; i++) | |
{ | |
if (clientId.charAt(i) == separator) | |
{ | |
// We found an ancestor NamingContainer client id - add | |
// an entry to the map. | |
String namingContainerClientId = clientId.substring(0, i); | |
// Check to see whether we've already ids under this | |
// NamingContainer client id. If not, create the | |
// Collection for this NamingContainer client id and | |
// stash it away in our map | |
Collection<String> c = _subtreeClientIds | |
.get(namingContainerClientId); | |
if (c == null) | |
{ | |
// TODO: smarter initial size? | |
c = new ArrayList<String>(); | |
_subtreeClientIds.put(namingContainerClientId, c); | |
} | |
// Stash away the client id | |
c.add(clientId); | |
} | |
} | |
} | |
// Given a single client id, remove any entries corresponding | |
// entries from our subtree collections | |
private void _removeSubtreeClientId(String clientId) | |
{ | |
// Loop through each entry in the map and check to see whether | |
// the client id to remove should be contained in the corresponding | |
// collection - ie. whether the key (the NamingContainer client id) | |
// is present at the start of the client id to remove. | |
for (String key : _subtreeClientIds.keySet()) | |
{ | |
if (clientId.startsWith(key)) | |
{ | |
// If the clientId starts with the key, we should | |
// have an entry for this clientId in the corresponding | |
// collection. Remove it. | |
Collection<String> ids = _subtreeClientIds.get(key); | |
ids.remove(clientId); | |
} | |
} | |
} | |
// Little proxy collection implementation. We proxy the id | |
// collection so that we can detect modifications and update | |
// our internal state when ids to visit are added or removed. | |
private class CollectionProxy<E extends String> extends | |
AbstractCollection<E> | |
{ | |
private CollectionProxy(Collection<E> wrapped) | |
{ | |
_wrapped = wrapped; | |
} | |
@Override | |
public int size() | |
{ | |
return _wrapped.size(); | |
} | |
@Override | |
public Iterator<E> iterator() | |
{ | |
return new IteratorProxy<E>(_wrapped.iterator()); | |
} | |
@Override | |
public boolean add(E o) | |
{ | |
boolean added = _wrapped.add(o); | |
if (added) | |
{ | |
_idAdded(o); | |
} | |
return added; | |
} | |
private final Collection<E> _wrapped; | |
} | |
// Little proxy iterator implementation used by CollectionProxy | |
// so that we can catch removes. | |
private class IteratorProxy<E extends String> implements Iterator<E> | |
{ | |
private IteratorProxy(Iterator<E> wrapped) | |
{ | |
_wrapped = wrapped; | |
} | |
public boolean hasNext() | |
{ | |
return _wrapped.hasNext(); | |
} | |
public E next() | |
{ | |
_current = _wrapped.next(); | |
return _current; | |
} | |
public void remove() | |
{ | |
if (_current != null) | |
{ | |
_idRemoved(_current); | |
} | |
_wrapped.remove(); | |
} | |
private final Iterator<E> _wrapped; | |
private E _current = null; | |
} | |
// The client ids to visit | |
private final Collection<String> _clientIds; | |
// The ids to visit | |
private final Collection<String> _ids; | |
// The client ids that have yet to be visited | |
private final Collection<String> _unvisitedClientIds; | |
// This map contains the information needed by getIdsToVisit(). | |
// The keys in this map are NamingContainer client ids. The values | |
// are collections containing all of the client ids to visit within | |
// corresponding naming container. | |
private final Map<String, Collection<String>> _subtreeClientIds; | |
// The FacesContext for this request | |
private final FacesContext _facesContext; | |
// Our visit hints | |
private final Set<VisitHint> _hints; | |
} |