blob: 54e04c00e7400adae1da118fe0d739155e1bb582 [file] [log] [blame]
/*
* 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;
}