blob: 11fb02c6304df2081d6969f90ea5d5ff90029c62 [file] [log] [blame]
/*
* Copyright 2004-2005 The Apache Software Foundation or its licensors,
* as applicable.
*
* Licensed 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.jackrabbit.classloader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jackrabbit.util.ChildrenCollectorFilter;
import org.apache.jackrabbit.util.Text;
/**
* The <code>PatternPath</code> implements a list of repository item path
* patterns providing an iterator on the expanded paths. The list of patterns is
* immutably configured from an array of strings.
* <p>
* While the original list of path patterns may be retrieved for informational
* purposes by calling the {@link #getPath()} method, the primary contents of
* instances of this class are the expanded paths accessible by calling the
* {@link #getExpandedPaths()} method.
* <p>
* Please note that though this list is immutable there is intentionally no
* guarantee that all invocations of the {@link #getExpandedPaths} method
* return the same contents as the patterns contained in the list may expand to
* different paths for each invocation of that method.
* <p>
* Each entry in the pattern list is a path whose segments conform to the
* pattern syntax defined for the <code>Node.getNodes(String)</code> method.
* The pattern may be a full name or a partial name with one or more wildcard
* characters ("*"), or a disjunction (using the "|" character to represent
* logical <i>OR</i>) of these. For example,
* <blockquote><code>"jcr:*|foo:bar"</code></blockquote>
* would match <code>"foo:bar"</code>, but also <code>"jcr:whatever"</code>.
* <p>
* The EBNF for pattern is:
* <pre>
* namePattern ::= disjunct {'|' disjunct}
* disjunct ::= name [':' name]
* name ::= '*' |
* ['*'] fragment {'*' fragment}['*']
* fragment ::= char {char}
* char ::= nonspace | ' '
* nonspace ::= (* Any Unicode character except:
* '/', ':', '[', ']', '*',
* ''', '"', '|' or any whitespace
* character *)
* </pre>
*
* @author Felix Meschberger
* @version $Rev:$, $Date:$
*/
/* package */ class PatternPath {
/** default logger */
private static final Log log = LogFactory.getLog(PatternPath.class);
/** The session to access the repository */
private final Session session;
/** The list of path patterns */
private final String[] patterns;
/**
* Creates a <code>PatternPath</code> containing the elements of the
* string array. Each entry in the array which is either empty or
* <code>null</code> is ignored and not added to the list. If the array
* is empty or only contains empty or <code>null</code> elements, the
* resulting list will consequently be empty.
*
* @param session The session to access the Repository to expand the paths
* and to register as an event listener.
* @param pathPatterns The array of path patterns to add.
*
* @throws NullPointerException if the <code>pathPatterns</code> array or
* the <code>session</code> is <code>null</code>.
*/
/* package */ PatternPath(Session session, String[] pathPatterns) {
// check session
if (session == null) {
throw new NullPointerException("session");
}
// prepare the pattern list, excluding null/empty entries
List patternList = new ArrayList();
for (int i=0; i < pathPatterns.length; i++) {
addChecked(patternList, pathPatterns[i]);
}
patterns =
(String[]) patternList.toArray(new String[patternList.size()]);
this.session = session;
}
/**
* Returns the session from which this instance has been constructed.
*/
/* package */ Session getSession() {
return session;
}
/**
* Returns a copy of the list of path patterns from which this instance has
* been constructed.
*/
/* package */ String[] getPath() {
return (String[]) patterns.clone();
}
/**
* Returns the list of expanded paths matching the list of patterns. This
* list is guaranteed to only return existing items.
* <p>
* Each invocation of this method expands the pattern anew and returns a
* new list instance.
*
* @return The list of paths matching the patterns. If the pattern list is
* empty or if no real paths match for any entry in the list, the
* returned list is empty.
*
* @throws RepositoryException if an error occurrs expanding the path
* pattern list.
*/
/* package */ List getExpandedPaths() throws RepositoryException {
List result = new ArrayList(patterns.length);
Node root = session.getRootNode();
for (int i=0; i < patterns.length; i++) {
String entry = patterns[i];
if (entry.indexOf('*') >= 0 || entry.indexOf('|') >= 0) {
scan(root, entry, result);
} else {
// add path without pattern characters without further
// checking. This allows adding paths which do not exist yet.
result.add(entry);
}
}
return result;
}
//---------- Object overwrite ----------------------------------------------
/**
* Returns <code>true</code> if this object equals the other object. This
* implementation only returns true if the other object is the same as this
* object.
* <p>
* The difference to the base class implementation is, that we only accept
* equality if the other object is the same than this object. This is
* actually the same implementation as the original <code>Object.equals</code>
* implementation.
*
* @param o The other object to compare to.
*
* @return <code>true</code> if the other object is the same as this.
*/
public boolean equals(Object o) {
return o == this;
}
/**
* Returns a hashcode for this instance. This is currently the hash code
* returned by the parent implementation. While it does not violate the
* contract to not change the <code>hashCode()</code> implementation but
* to change the implementation of the {@link #equals} method, I think this
* is ok, because our implementation of the {@link #equals} method is just
* more specific than the base class implementation, which also allows
* the other object to be a list with the same contents.
*
* @return The hash code returned by the base class implementation.
*/
public int hashCode() {
return super.hashCode();
}
/**
* Returns a string representation of this instance. This is actually the
* result of the string representation of the List, this actually is,
* prefixed with the name of this class.
*
* @return The string representation of this instance.
*/
public String toString() {
StringBuffer buf = new StringBuffer("PatternPath: [");
for (int i=0; i < patterns.length; i++) {
if (i != 0) buf.append(", ");
buf.append(patterns[i]);
}
buf.append("]");
return buf.toString();
}
//---------- internal ------------------------------------------------------
/**
* Adds the string to the list of patterns, if neither empty nor
* <code>null</code>. If the string has one or more trailing slashes
* (<em>/</em>) they are removed before adding the string.
*/
private void addChecked(List patternList, String pattern) {
if (pattern == null || pattern.length() == 0) {
log.debug("addChecked: Not adding null/empty pattern");
} else {
// remove all trailing slashes
while (pattern.endsWith("/") && pattern.length() > 1) {
pattern = pattern.substring(0, pattern.length()-1);
}
log.debug("addChecked: Adding " + pattern);
patternList.add(pattern);
}
}
//---------- Path expansion -----------------------------------------------
/**
* Finds the paths of all nodes and properties matching the pattern below
* the <code>root</code> node.
*
* @param root The root node of the subtree to match against the path
* pattern.
* @param pathPattern The path pattern to use to find matching nodes.
* @param gather The list into which the paths of matching child items
* are added.
*/
private static void scan(Node root, String pathPattern, List gather)
throws RepositoryException {
// initial list of candidates is the root node
List candidates = new ArrayList();
candidates.add(root);
StringTokenizer patterns = new StringTokenizer(pathPattern, "/");
boolean moreTokens = patterns.hasMoreTokens();
while (moreTokens) {
String pattern = patterns.nextToken();
moreTokens = patterns.hasMoreTokens();
// new candidates are the children of the current candidates list
// matching the current pattern
List newCandidates = new ArrayList();
for (Iterator ci=candidates.iterator(); ci.hasNext(); ) {
Node current = (Node) ci.next();
for (NodeIterator ni=current.getNodes(pattern); ni.hasNext(); ) {
newCandidates.add(ni.nextNode());
}
// if pattern is the last, also consider properties
if (!moreTokens) {
PropertyIterator pi = current.getProperties(pattern);
while (pi.hasNext()) {
newCandidates.add(pi.nextProperty());
}
}
}
// drop old candidates and use new for next step
candidates.clear();
candidates = newCandidates;
}
// add paths of the candidates to the gather list
for (Iterator ci=candidates.iterator(); ci.hasNext(); ) {
Item current = (Item) ci.next();
gather.add(current.getPath());
}
}
//---------- matching support ---------------------------------------------
/**
* Applies the list of path patterns to the given path returning
* <code>true</code> if it matches, <code>false</code> otherwise.
* <p>
* <b><em>This method is package protected for testing purposes. This
* method is not intended to be used by clients. Its specification or
* implementation may change without notice.</em></b>
*
* @param path The path to match with the pattern list.
*
* @return <code>true</code> if the path matches any of the patterns.
*/
/* package */ boolean matchPath(String path) {
StringTokenizer exploded = new StringTokenizer(path, "/");
OUTER_LOOP:
for (int i=0; i < patterns.length; i++) {
StringTokenizer exEntry = new StringTokenizer(patterns[i], "/");
// ignore if the number of path elements to not match
if (exploded.countTokens() != exEntry.countTokens()) {
continue;
}
while (exploded.hasMoreTokens()) {
if (!ChildrenCollectorFilter.matches(exploded.nextToken(),
exEntry.nextToken())) {
continue OUTER_LOOP;
}
}
// if I get here, the path matches entry[i]
return true;
}
// if we run out, no match has been found
return false;
}
}