Index: jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLProvider.java =================================================================== --- jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLProvider.java (revision 645404) +++ jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLProvider.java (working copy) @@ -485,7 +485,7 @@ Path parentPath = absPath.getAncestor(1); while (nid == null) { nid = session.getHierarchyManager().resolveNodePath(parentPath); - if (parentPath.getDepth() == 1) { + if (parentPath.getDepth() == Path.ROOT_DEPTH) { // root-node reached break; } else { Index: jackrabbit-core/src/test/java/org/apache/jackrabbit/core/CachingHierarchyManagerTest.java =================================================================== --- jackrabbit-core/src/test/java/org/apache/jackrabbit/core/CachingHierarchyManagerTest.java (revision 645404) +++ jackrabbit-core/src/test/java/org/apache/jackrabbit/core/CachingHierarchyManagerTest.java (working copy) @@ -44,7 +44,7 @@ ItemStateManager provider = new MyItemStateManager(); cache = new CachingHierarchyManager(rootNodeId, provider, null); PathFactory factory = PathFactoryImpl.getInstance(); - final Path path = factory.create("{}\t{}"); + final Path path = factory.getRootPath(); for(int i=0; i<3; i++) { new Thread(new Runnable() { public void run() { Index: jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathFactoryImpl.java =================================================================== --- jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathFactoryImpl.java (revision 645404) +++ jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathFactoryImpl.java (working copy) @@ -51,6 +51,8 @@ * the root path */ private static final Path ROOT = new PathImpl(new Path.Element[]{ROOT_ELEMENT}, true); + private static final Path CURRENT_PATH = new PathImpl(new Path.Element[]{CURRENT_ELEMENT}, true); + private static final Path PARENT_PATH = new PathImpl(new Path.Element[]{PARENT_ELEMENT}, true); private PathFactoryImpl() {} @@ -332,7 +334,7 @@ /** * @see Path#getNormalizedPath() */ - public Path getNormalizedPath() throws RepositoryException { + public Path getNormalizedPath() { if (isNormalized()) { return this; } @@ -341,11 +343,6 @@ for (int i = 0; i < elements.length; i++) { Path.Element elem = elements[i]; if (elem.denotesParent() && !last.denotesParent()) { - if (last.denotesRoot()) { - // the first element is the root element; - // ".." would refer to the parent of root - throw new RepositoryException("Path can not be canonicalized: unresolvable '..' element"); - } queue.removeLast(); if (queue.isEmpty()) { last = PARENT_ELEMENT; @@ -358,7 +355,7 @@ } } if (queue.isEmpty()) { - throw new RepositoryException("Path can not be normalized: would result in an empty path."); + return CURRENT_PATH; } boolean isNormalized = true; return new PathImpl((Path.Element[]) queue.toArray(new Element[queue.size()]), isNormalized); @@ -396,8 +393,7 @@ if (p0.equals(p1)) { // both paths are equal, the relative path is therefore '.' - Builder pb = new Builder(new Path.Element[] {CURRENT_ELEMENT}); - return pb.getPath(); + return CURRENT_PATH; } // determine length of common path fragment @@ -436,22 +432,34 @@ if (degree < 0) { throw new IllegalArgumentException("degree must be >= 0"); } else if (degree == 0) { - return this; + return this.getNormalizedPath(); } - int length = elements.length - degree; - if (length < 1) { - throw new PathNotFoundException("no such ancestor path of degree " + degree); + + if (isAbsolute()) { + Path.Element[] normElems = getNormalizedPath().getElements(); + int length = normElems.length - degree; + if (length < 1) { + throw new PathNotFoundException("no such ancestor path of degree " + degree); + } + Path.Element[] ancestorElements = new Element[length]; + System.arraycopy(normElems, 0, ancestorElements, 0, length); + return new PathImpl(ancestorElements, true); + } else { + Path.Element[] ancestorElements = new Element[elements.length + degree]; + System.arraycopy(elements, 0, ancestorElements, 0, elements.length); + + for (int i = elements.length; i < ancestorElements.length; i++) { + ancestorElements[i] = PARENT_ELEMENT; + } + return new PathImpl(ancestorElements, false).getNormalizedPath(); } - Path.Element[] elements = new Element[length]; - System.arraycopy(this.elements, 0, elements, 0, length); - return new PathImpl(elements, normalized); } /** * @see Path#getAncestorCount() */ public int getAncestorCount() { - return getDepth() - 1; + return (isAbsolute()) ? getDepth() : -1; } /** @@ -469,7 +477,8 @@ for (int i = 0; i < elements.length; i++) { if (elements[i].denotesParent()) { depth--; - } else if (!elements[i].denotesCurrent()) { + } else if (elements[i].denotesName()) { + // don't count root/current element. depth++; } } @@ -477,38 +486,53 @@ } /** - * @see Path#isAncestorOf(Path) + * @see Path#isEquivalentTo(Path) */ - public boolean isAncestorOf(Path other) throws IllegalArgumentException, RepositoryException { + public boolean isEquivalentTo(Path other) throws RepositoryException { if (other == null) { throw new IllegalArgumentException("null argument"); } - // make sure both paths are either absolute or relative if (isAbsolute() != other.isAbsolute()) { throw new IllegalArgumentException("Cannot compare a relative path with an absolute path"); } - // make sure we're comparing normalized paths - Path p0 = getNormalizedPath(); - Path p1 = other.getNormalizedPath(); - if (p0.equals(p1)) { + if (getDepth() != other.getDepth()) { return false; } - // calculate depth of paths (might be negative) - if (p0.getDepth() >= p1.getDepth()) { + + Element[] elems0 = getNormalizedPath().getElements(); + Element[] elems1 = other.getNormalizedPath().getElements(); + + if (elems0.length != elems1.length) return false; - } - Path.Element[] elems0 = p0.getElements(); - Path.Element[] elems1 = p1.getElements(); - for (int i = 0; i < elems0.length; i++) { - if (!elems0[i].equals(elems1[i])) { + + for (int k = 0; k < elems0.length; k++) { + if (!elems0[k].equals(elems1[k])) return false; - } } return true; } /** + * @see Path#isAncestorOf(Path) + */ + public boolean isAncestorOf(Path other) throws IllegalArgumentException, RepositoryException { + if (other == null) { + throw new IllegalArgumentException("null argument"); + } + // make sure both paths are either absolute or relative + if (isAbsolute() != other.isAbsolute()) { + throw new IllegalArgumentException("Cannot compare a relative path with an absolute path"); + } + + int delta = other.getDepth() - getDepth(); + if (delta <= 0) + return false; + + return isEquivalentTo(other.getAncestor(delta)); + } + + /** * @see Path#isDescendantOf(Path) */ public boolean isDescendantOf(Path other) throws IllegalArgumentException, RepositoryException { @@ -522,7 +546,7 @@ * @see Path#subPath(int, int) */ public Path subPath(int from, int to) throws IllegalArgumentException, RepositoryException { - if (from < 0 || to >= elements.length || from >= to) { + if (from < 0 || to > elements.length || from >= to) { throw new IllegalArgumentException(); } if (!isNormalized()) { @@ -843,14 +867,9 @@ /** * flag indicating if the current path is normalized */ - private boolean isNormalized = true; + private boolean isNormalized; /** - * flag indicating if the current path has leading parent '..' elements - */ - private boolean leadingParent = true; - - /** * Creates a new Builder and initialized it with the given path * elements. * @@ -874,11 +893,35 @@ if (elements == null || elements.length == 0) { throw new IllegalArgumentException("Cannot build path from null or 0 elements."); } + this.elements = elements; - for (int i = 0; i < elements.length; i++) { - Path.Element elem = elements[i]; - leadingParent &= elem.denotesParent(); - isNormalized &= !elem.denotesCurrent() && (leadingParent || !elem.denotesParent()); + if (elements.length == 1) { + isNormalized = true; + } else { + boolean absolute = elements[0].denotesRoot(); + isNormalized = true; + int parents = 0; + int named = 0; + for (int i = 0; i < elements.length; i++) { + Path.Element elem = elements[i]; + if (elem.denotesName()) { + named++; + } else if (elem.denotesRoot()) { + if (i > 0) { + throw new IllegalArgumentException("Invalid path: The root element may only occur at the beginning."); + } + } else if (elem.denotesParent()) { + parents++; + if (absolute || named > 0) { + isNormalized = false; + } + } else /* current element */ { + isNormalized = false; + } + } + if (absolute && parents > named) { + throw new IllegalArgumentException("Invalid path: Too many parent elements."); + } } } @@ -888,8 +931,22 @@ * @return a new {@link Path} */ private Path getPath() { + // special path with a single element + if (elements.length == 1) { + if (elements[0].denotesRoot()) { + return PathFactoryImpl.ROOT; + } + if (elements[0].denotesParent()) { + return PathFactoryImpl.PARENT_PATH; + } + if (elements[0].denotesCurrent()) { + return PathFactoryImpl.CURRENT_PATH; + } + } + + // default: build a new path // no need to check the path format, assuming all names correct return new PathImpl(elements, isNormalized); } } -} \ No newline at end of file +} Index: jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/conversion/ParsingPathResolverTest.java =================================================================== --- jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/conversion/ParsingPathResolverTest.java (revision 645404) +++ jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/conversion/ParsingPathResolverTest.java (working copy) @@ -95,10 +95,6 @@ assertValidPath("../a/b/../../../../f"); assertValidPath("a/../.."); assertValidPath("../../a/."); - - // TODO: Should these paths be detected as invalid by the parser? - assertValidPath("/.."); - assertValidPath("/a/b/../../.."); } /** Index: jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/conversion/PathParserTest.java =================================================================== --- jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/conversion/PathParserTest.java (revision 645404) +++ jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/conversion/PathParserTest.java (working copy) @@ -28,8 +28,6 @@ import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.PathFactory; -import javax.jcr.RepositoryException; - /** * PathParserTest */ @@ -57,7 +55,7 @@ Path p = PathParser.parse(t.path, resolver, factory); if (t.normalizedPath==null) { if (!t.isValid()) { - fail("Should throw MalformedPathException: " + t.path); + fail("Should throw IllegalArgumentException: " + t.path); } assertEquals("\"" + t.path + "\".create(false)", t.path, pathResolver.getJCRPath(p)); assertEquals("\"" + t.path + "\".isNormalized()", t.isNormalized(), p.isNormalized()); @@ -66,12 +64,12 @@ // check with normalization p = p.getNormalizedPath(); if (!t.isValid()) { - fail("Should throw MalformedPathException: " + t.path); + fail("Should throw IllegalArgumentException: " + t.path); } assertEquals("\"" + t.path + "\".create(true)", t.normalizedPath, pathResolver.getJCRPath(p)); assertEquals("\"" + t.path + "\".isAbsolute()", t.isAbsolute(), p.isAbsolute()); } - } catch (RepositoryException e) { + } catch (Exception e) { if (t.isValid()) { System.out.println(t.path); throw e; @@ -108,7 +106,7 @@ } } - public void testNormalizedPaths() throws Exception { + public void testNormalizedPaths() throws Exception { List paths = new ArrayList(); // normalized paths paths.add(PathParser.parse("/", resolver, factory)); @@ -118,6 +116,7 @@ paths.add(PathParser.parse("foo", resolver, factory)); paths.add(PathParser.parse("../../foo/bar", resolver, factory)); paths.add(PathParser.parse("..", resolver, factory)); + paths.add(PathParser.parse(".", resolver, factory)); for (Iterator it = paths.iterator(); it.hasNext(); ) { Path path = (Path) it.next(); @@ -132,7 +131,6 @@ paths.add(PathParser.parse("/foo/../bar", resolver, factory)); paths.add(PathParser.parse("/foo/./bar", resolver, factory)); paths.add(PathParser.parse("./foo", resolver, factory)); - paths.add(PathParser.parse(".", resolver, factory)); paths.add(PathParser.parse("foo/..", resolver, factory)); paths.add(PathParser.parse("../foo/..", resolver, factory)); paths.add(PathParser.parse("../foo/.", resolver, factory)); @@ -202,7 +200,6 @@ paths.add(PathParser.parse("./foo", resolver, factory)); paths.add(PathParser.parse(".", resolver, factory)); paths.add(PathParser.parse("/foo/..", resolver, factory)); - paths.add(PathParser.parse("/../foo/..", resolver, factory)); paths.add(PathParser.parse("/../foo/.", resolver, factory)); for (Iterator it = paths.iterator(); it.hasNext(); ) { Index: jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/JcrPath.java =================================================================== --- jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/JcrPath.java (revision 645404) +++ jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/JcrPath.java (working copy) @@ -42,12 +42,12 @@ list.add(new JcrPath("/prefix:name/prefix:name", NOR|VAL)); list.add(new JcrPath("/name[2]/name[2]", NOR|VAL)); list.add(new JcrPath("/prefix:name[2]/prefix:name[2]", NOR|VAL)); - list.add(new JcrPath("a/b/c/", "a/b/c", NOR|VAL)); list.add(new JcrPath("/a/b/c/", "/a/b/c", NOR|VAL)); list.add(new JcrPath("/a/..../", "/a/....", NOR|VAL)); list.add(new JcrPath("/a/b:.a./", "/a/b:.a.", NOR|VAL)); // relative paths + list.add(new JcrPath("a/b/c/", "a/b/c", NOR|VAL)); list.add(new JcrPath("a/b/c", NOR|VAL)); list.add(new JcrPath("prefix:name/prefix:name", NOR|VAL)); list.add(new JcrPath("name[2]/name[2]", NOR|VAL)); @@ -57,6 +57,8 @@ // invalid paths list.add(new JcrPath("")); + list.add(new JcrPath("//")); + list.add(new JcrPath("/a//b")); list.add(new JcrPath(" /a/b/c/")); list.add(new JcrPath("/a/b/c/ ")); list.add(new JcrPath("/:name/prefix:name")); @@ -69,9 +71,19 @@ list.add(new JcrPath(":name/prefix:name")); list.add(new JcrPath("name[0]/name[2]")); list.add(new JcrPath("prefix:name[2]foo/prefix:name[2]")); + list.add(new JcrPath("/..", "/..", 0)); + list.add(new JcrPath("/a/b/../../..", "/a/b/../../..", 0)); + // normalized, relative paths + list.add(new JcrPath(".", ".", NOR|VAL)); + list.add(new JcrPath("..", "..", NOR|VAL)); + list.add(new JcrPath("../..", "../..", NOR|VAL)); + list.add(new JcrPath("../../a/b", "../../a/b", NOR|VAL)); + list.add(new JcrPath("../a", "../a",NOR|VAL)); + // not normalized paths list.add(new JcrPath("/a/../b", "/b", VAL)); + list.add(new JcrPath("/a/../b/./c/d/..", "/b/c", VAL)); list.add(new JcrPath("./../.", "..", VAL)); list.add(new JcrPath("/a/./b", "/a/b", VAL)); list.add(new JcrPath("/a/b/../..", "/", VAL)); @@ -80,9 +92,13 @@ list.add(new JcrPath("a/../..", "..", VAL)); list.add(new JcrPath("../../a/.", "../../a", VAL)); - // invalid normalized paths - list.add(new JcrPath("/..", "/..", 0)); - list.add(new JcrPath("/a/b/../../..", "/a/b/../../..", 0)); + // other non-normalized, relative path + list.add(new JcrPath("./.", ".", VAL)); + list.add(new JcrPath("./a", "a", VAL)); + list.add(new JcrPath("a/..", ".", VAL)); + list.add(new JcrPath("../a/..", "..", VAL)); + list.add(new JcrPath("../a/.", "../a", VAL)); + list.add(new JcrPath("a/./b", "a/b", VAL)); } /** @@ -104,7 +120,7 @@ public JcrPath(String path, String normalizedPath, int flags) { this.path = path; this.normalizedPath = normalizedPath; - this.flags = flags | ((path.length()>0 && path.charAt(0)=='/') ? ABS : 0); + this.flags = flags | ((path.length() > 0 && path.charAt(0)=='/') ? ABS : 0); } public static JcrPath[] getTests() { @@ -139,4 +155,4 @@ } return b.toString(); } -} \ No newline at end of file +} Index: jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/PathFactoryTest.java =================================================================== --- jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/PathFactoryTest.java (revision 0) +++ jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/PathFactoryTest.java (revision 0) @@ -0,0 +1,283 @@ +/* + * 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.jackrabbit.spi.commons.name; + +import junit.framework.TestCase; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.NamespaceException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * PathFactoryTest... + */ +public class PathFactoryTest extends TestCase { + + private PathFactory factory; + private PathResolver resolver; + + protected void setUp() throws Exception { + super.setUp(); + factory = PathFactoryImpl.getInstance(); + + NamespaceResolver nsresolver = new NamespaceResolver() { + public String getURI(String prefix) throws NamespaceException { + throw new UnsupportedOperationException(); + } + public String getPrefix(String uri) throws NamespaceException { + if (uri.equals(Name.NS_JCR_URI)) { + return Name.NS_JCR_PREFIX; + } else { + return uri; + } + } + }; + resolver = new DefaultNamePathResolver(nsresolver); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + private String getString(Path p) throws NamespaceException { + return resolver.getJCRPath(p); + } + + public void testCreateNullName() { + try { + factory.create((Name) null); + fail("Creating with null name is invalid"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCreateNullNameIndex() { + try { + factory.create(null, 1); + fail("Creating with null name is invalid"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCreateElementNullName() { + try { + factory.createElement(null); + fail("Creating element with null name is invalid"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCreateElementNullNameIndex() { + try { + factory.createElement(null, 1); + fail("Creating element with null name is invalid"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCreateWithInvalidIndex() { + try { + factory.create(NameConstants.JCR_NAME, -1); + fail("-1 is an invalid index"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCreateElementWithInvalidIndex() { + try { + factory.createElement(NameConstants.JCR_NAME, -1); + fail("-1 is an invalid index"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testRoot() { + assertTrue(factory.getRootPath().isAbsolute()); + assertTrue(factory.getRootPath().isNormalized()); + } + + public void testCreateRoot() { + Path root = factory.getRootPath(); + Path.Element rootElement = factory.getRootElement(); + Name rootName = rootElement.getName(); + + assertEquals(root, factory.create(rootName)); + assertEquals(root, factory.create(new Path.Element[] {rootElement})); + assertEquals(root, factory.create(root.toString())); + assertEquals(root, factory.create(new Path.Element[] {factory.createElement(rootName)})); + + try { + factory.create(rootName, 1); + fail("Cannot create path from root name with a specific index."); + } catch (IllegalArgumentException e) { + // ok + } + try { + factory.createElement(rootName, 1); + fail("Cannot create element from root name with a specific index."); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCurrent() { + Path.Element currElem = factory.getCurrentElement(); + Name currName = currElem.getName(); + + assertEquals(currElem, factory.createElement(currName)); + + Path current = factory.create(new Path.Element[] {currElem}); + assertEquals(current, factory.create(currName)); + assertFalse(current.isAbsolute()); + assertTrue(current.isNormalized()); + + try { + factory.createElement(currName, 1); + fail("Cannot create current element with an index."); + } catch (IllegalArgumentException e) { + // ok + } + try { + factory.create(currName, 1); + fail("Cannot create current path with an index."); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testParent() { + Path.Element parentElem = factory.getParentElement(); + Name parentName = parentElem.getName(); + + assertEquals(parentElem, factory.createElement(parentName)); + + Path parent = factory.create(new Path.Element[] {parentElem}); + assertEquals(parent, factory.create(parentName)); + assertFalse(parent.isAbsolute()); + assertTrue(parent.isNormalized()); + + try { + factory.createElement(parentName, 1); + fail("Cannot create parent element with an index."); + } catch (IllegalArgumentException e) { + // ok + } + try { + factory.create(parentName, 1); + fail("Cannot create parent path with an index."); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testCreateInvalidPath() throws NamespaceException { + + Path.Element rootEl = factory.getRootElement(); + Path.Element pe = factory.getParentElement(); + Path.Element ce = factory.getCurrentElement(); + Path.Element element = factory.createElement(NameConstants.JCR_NAME, 3); + Path.Element element2 = factory.createElement(NameConstants.JCR_DATA, 3); + + List elementArrays = new ArrayList(); + elementArrays.add(new Path.Element[]{rootEl, rootEl}); + elementArrays.add(new Path.Element[] {element, rootEl, pe}); + elementArrays.add(new Path.Element[] {pe, rootEl, element}); + elementArrays.add(new Path.Element[] {pe, rootEl, element}); + elementArrays.add(new Path.Element[] {rootEl, pe}); + elementArrays.add(new Path.Element[] {rootEl, element, element2, pe, pe, pe}); + + for (Iterator it = elementArrays.iterator(); it.hasNext(); ) { + try { + Path p = factory.create((Path.Element[]) it.next()); + fail("Invalid path " + getString(p)); + } catch (IllegalArgumentException e) { + // ok + } + } + } + + public void testCreateInvalidPath2() { + Path root = factory.getRootPath(); + Name rootName = factory.getRootElement().getName(); + Name parentName = factory.getParentElement().getName(); + + List list = new ArrayList(); + list.add(new ParentPathNameIndexDoNormalize(root, rootName, -1, true)); + list.add(new ParentPathNameIndexDoNormalize(root, rootName, -1, false)); + list.add(new ParentPathNameIndexDoNormalize(root, rootName, 3, false)); + list.add(new ParentPathNameIndexDoNormalize(factory.create(parentName), rootName, 3, true)); + + for (Iterator it = list.iterator(); it.hasNext();) { + ParentPathNameIndexDoNormalize test = (ParentPathNameIndexDoNormalize) it.next(); + try { + if (test.index == -1) { + Path p = factory.create(test.parentPath, test.name, test.doNormalize); + } else { + Path p = factory.create(test.parentPath, test.name, test.index, test.doNormalize); + } + fail("Invalid path " + test.parentPath + " + " + test.name); + } catch (Exception e) { + // ok + } + } + } + + public void testCreateInvalidPath3() { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (!tests[i].isValid()) { + try { + Path p = resolver.getQPath(tests[i].path); + fail("Invalid path " + getString(p)); + } catch (Exception e) { + // ok + } + } + } + } + + //-------------------------------------------------------------------------- + private static class ParentPathNameIndexDoNormalize { + + private final Path parentPath; + private final Name name; + private final int index; + private final boolean doNormalize; + + private ParentPathNameIndexDoNormalize(Path parentPath, Name name, + int index, boolean doNormalize) { + this.parentPath = parentPath; + this.name = name; + this.index = index; + this.doNormalize = doNormalize; + } + } +} Index: jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/PathTest.java =================================================================== --- jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/PathTest.java (revision 645404) +++ jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/PathTest.java (working copy) @@ -16,17 +16,24 @@ */ package org.apache.jackrabbit.spi.commons.name; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; + import junit.framework.TestCase; + import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; import org.apache.jackrabbit.spi.commons.conversion.ParsingNameResolver; import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; import org.apache.jackrabbit.spi.commons.conversion.PathResolver; import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.util.Text; -import javax.jcr.NamespaceException; -import javax.jcr.RepositoryException; - /** * PathTest... */ @@ -41,7 +48,8 @@ return uri; } }; - private static final PathResolver resolver = new ParsingPathResolver(factory, new ParsingNameResolver(NameFactoryImpl.getInstance(), nsResolver)); + private static final NameResolver nameResolver = new ParsingNameResolver(NameFactoryImpl.getInstance(), nsResolver); + private static final PathResolver resolver = new ParsingPathResolver(factory, nameResolver); public void testRootIsDescendantOfRoot() throws RepositoryException { Path root = factory.getRootPath(); @@ -52,6 +60,111 @@ assertFalse(root.isAncestorOf(root)); } + public void testGetAncestor() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + if (p.getNormalizedPath().denotesRoot()) { + continue; + } + + String jcrAncestor = Text.getRelativeParent(resolver.getJCRPath(p.getNormalizedPath()), 1); + Path ancestor = resolver.getQPath(jcrAncestor); + assertEquals(ancestor, p.getAncestor(1)); + } + } + } + + public void testGetAncestorOfRelativePath() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + int degree = 5; + for (int i = 0; i < tests.length; i++) { + JcrPath test = tests[i]; + if (test.isValid() && !test.isAbsolute()) { + Path p = resolver.getQPath(test.path); + + StringBuffer expJcrAnc = new StringBuffer(test.path); + expJcrAnc.append((test.path.endsWith("/") ? "" : "/")); + expJcrAnc.append("../../../../.."); + + Path ancestor = resolver.getQPath(expJcrAnc.toString()).getNormalizedPath(); + assertEquals(ancestor, p.getAncestor(5).getNormalizedPath()); + } + } + } + + public void testGetAncestorAtDegreeDepth() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + JcrPath test = tests[i]; + if (test.isValid() && test.isAbsolute()) { + Path p = resolver.getQPath(test.path); + + int degree = p.getDepth(); + if (degree > 0) { + assertTrue(p.getAncestor(degree).denotesRoot()); + } + } + } + } + + public void testGetAncestorIsAncestor() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + while (!p.getNormalizedPath().denotesRoot()) { + Path ancestor = p.getAncestor(1); + assertTrue(ancestor.isAncestorOf(p)); + p = ancestor; + } + } + } + } + + public void testGetAncestorOfRelativePath2() throws RepositoryException { + for (Iterator it = JcrPathAndAncestor.list.iterator(); it.hasNext();) { + JcrPathAndAncestor tp = (JcrPathAndAncestor) it.next(); + + Path ancestor = resolver.getQPath(tp.ancestor).getNormalizedPath(); + Path p = resolver.getQPath(tp.path); + assertEquals("Expected ancestor " + tp.ancestor + " was " + tp.path + ".", + ancestor, p.getAncestor(tp.degree).getNormalizedPath()); + } + } + + public void testGetAncestorReturnsNormalized() throws RepositoryException { + List tests = JcrPathAndAncestor.list; + for (Iterator it = tests.iterator(); it.hasNext();) { + JcrPathAndAncestor test = (JcrPathAndAncestor) it.next(); + + Path p = resolver.getQPath(test.path); + assertTrue(p.getAncestor(test.degree).isNormalized()); + } + } + + public void testIsAncestorOfRelativePath() throws RepositoryException { + for (Iterator it = JcrPathAndAncestor.list.iterator(); it.hasNext();) { + JcrPathAndAncestor tp = (JcrPathAndAncestor) it.next(); + + Path ancestor = resolver.getQPath(tp.ancestor); + Path p = resolver.getQPath(tp.path); + + if (tp.degree == 0) { + assertFalse(tp.ancestor + " should not be ancestor of " + tp.path, + ancestor.isAncestorOf(p)); + } else { + assertTrue(tp.ancestor + " should be ancestor of " + tp.path, + ancestor.isAncestorOf(p)); + } + } + } + public void testAbsolutePathIsDescendantOfRoot() throws RepositoryException { Path root = factory.getRootPath(); JcrPath[] tests = JcrPath.getTests(); @@ -64,6 +177,7 @@ } } } + public void testRootIsAncestorOfAbsolutePath() throws RepositoryException { Path root = factory.getRootPath(); JcrPath[] tests = JcrPath.getTests(); @@ -77,6 +191,59 @@ } } + public void testIsEquivalentToSelf() throws RepositoryException { + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid()) { + Path p = resolver.getQPath(tests[i].path); + assertTrue(p.isEquivalentTo(p)); + } + } + } + + public void testIsEquivalentTo() throws IllegalArgumentException, RepositoryException { + for (Iterator it = Equivalent.list.iterator(); it.hasNext();) { + Equivalent tp = (Equivalent) it.next(); + + Path path = resolver.getQPath(tp.path); + Path other = resolver.getQPath(tp.other); + + if (tp.isEquivalent) { + assertTrue(tp.path + " should be equivalent to " + tp.other, + path.isEquivalentTo(other)); + } else { + assertFalse(tp.path + " should not be equivalent to " + tp.other, + path.isEquivalentTo(other)); + } + } + } + + public void testIsAncestorIsDescendant() throws RepositoryException { + Path absPath = factory.getRootPath(); + Path relPath = factory.create(NameConstants.JCR_DATA); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid()) { + Path p = resolver.getQPath(tests[i].path).getNormalizedPath(); + if (tests[i].isAbsolute()) { + if (absPath.isAncestorOf(p)) { + assertTrue(p.isDescendantOf(absPath)); + } else { + assertFalse(p.isDescendantOf(absPath)); + } + absPath = p; + } else { + if (relPath.isAncestorOf(p)) { + assertTrue(p.isDescendantOf(relPath)); + } else { + assertFalse(p.isDescendantOf(relPath)); + } + relPath = p; + } + } + } + } + /** * Test if IllegalArgumentException is thrown as expected. */ @@ -91,7 +258,7 @@ } /** - * Test if IllegalArgumentException is thrown as expected. + * Testing Path.isDescendantOf with rel/abs path where the path is abs/rel. */ public void testIsDescendantOfThrowsIllegalArgumentException() throws RepositoryException { Path abs = factory.create(factory.getRootPath(), NameConstants.JCR_DATA, true); @@ -116,20 +283,6 @@ } /** - * Test if RepositoryException is thrown as expected. - */ - public void testIsDescendantOfThrowsRepositoryException() throws RepositoryException { - Path abs = factory.create(NameConstants.JCR_DATA); - Path rel = factory.create(new Path.Element[] {factory.getCurrentElement()}); - try { - abs.isDescendantOf(rel); - fail("Path.isDescendantOf(Path) must throw RepositoryException if either path cannot be normalized."); - } catch (RepositoryException e) { - // ok. - } - } - - /** * Test if IllegalArgumentException is thrown as expected. */ public void testIsAncestorOfNull() throws RepositoryException { @@ -167,17 +320,416 @@ } } - /** - * Test if RepositoryException is thrown as expected. - */ - public void testIsAncestorOfThrowsRepositoryException() throws RepositoryException { - Path abs = factory.create(NameConstants.JCR_DATA); - Path rel = factory.create(new Path.Element[] {factory.getCurrentElement()}); + public void testAbsolutePaths() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + assertTrue("Path must be absolute " + tests[i].path, p.isAbsolute()); + } + } + } + + public void testNotAbsolutePaths() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && !tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + assertFalse("Path must not be absolute " + tests[i].path, p.isAbsolute()); + } + } + } + + public void testCanonicalPaths() throws Exception { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path);; + if (!tests[i].isNormalized()) { + p = p.getNormalizedPath(); + } + assertTrue("Path must be canonical " + tests[i].path, p.isCanonical()); + } + } + } + + public void testNotCanonicalPaths() throws Exception { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && (!tests[i].isNormalized() || !tests[i].isAbsolute())) { + Path p = resolver.getQPath(tests[i].path); + assertFalse("Path must not be canonical " + tests[i].path, p.isCanonical()); + } + } + } + + public void testIsNotAncestor() throws RepositoryException { + for (Iterator it = NotAncestor.list.iterator(); it.hasNext();) { + NotAncestor test = (NotAncestor) it.next(); + Path p = resolver.getQPath(test.path); + Path ancestor = resolver.getQPath(test.notAncestor); + assertFalse(test.notAncestor + " isn't an ancestor of " + test.path, + ancestor.isAncestorOf(p)); + } + } + + public void testDepth() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + String normJcrPath = (tests[i].normalizedPath == null) ? tests[i].path : tests[i].normalizedPath; + int depth = Text.explode(normJcrPath, '/').length; + assertTrue("Depth of " + tests[i].path + " must be " + depth, depth == p.getDepth()); + } + } + } + + public void testDepthOfRelativePath() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && !tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + int depth = Path.ROOT_DEPTH; + Path.Element[] elements = p.getNormalizedPath().getElements(); + for (int j = 0; j < elements.length; j++) { + if (elements[j].denotesParent()) { + depth--; + } else if (elements[j].denotesName()) { + depth++; + } + } + //System.out.println("Depth of " + tests[i].path + " = " + depth); + assertTrue("Depth of " + tests[i].path + " must be " + depth, depth == p.getDepth()); + } + } + } + + public void testDepthOfRoot() throws RepositoryException { + assertTrue("Depth of root must be " + Path.ROOT_DEPTH, + factory.getRootPath().getDepth() == Path.ROOT_DEPTH); + } + + public void testDepthOfCurrent() throws RepositoryException { + Path current = factory.create(factory.getCurrentElement().getName()); + assertTrue("Depth of current must be same as for root (" + Path.ROOT_DEPTH + ")", + current.getDepth() == Path.ROOT_DEPTH); + } + + public void testDepthOfParent() throws RepositoryException { + Path parent = factory.create(factory.getParentElement().getName()); + int depth = Path.ROOT_DEPTH - 1; + assertTrue("Depth of parent must be same as for root -1 (" + depth + ")", + parent.getDepth() == depth); + } + + public void testAncestorCount() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + assertTrue("Ancestor count must be same a depth", p.getDepth() == p.getAncestorCount()); + } + } + } + + public void testAncestorCountOfRelativePath() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && !tests[i].isAbsolute()) { + Path p = resolver.getQPath(tests[i].path); + assertTrue("Ancestor count or a relative path must be -1", -1 == p.getAncestorCount()); + } + } + } + + public void testAncestorCountOfRoot() throws RepositoryException { + assertTrue("AncestorCount of root must be " + 0, + factory.getRootPath().getAncestorCount() == 0); + } + + public void testAncestorCountOfCurrent() throws RepositoryException { + Path current = factory.create(factory.getCurrentElement().getName()); + assertTrue("AncestorCount of current must be -1", + current.getAncestorCount() == -1); + } + + public void testAncestorCountOfParent() throws RepositoryException { + Path parent = factory.create(factory.getParentElement().getName()); + assertTrue("AncestorCount of parent must be same as for -1", + parent.getAncestorCount() == - 1); + } + + public void testLength() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid()) { + int length = Text.explode(tests[i].path, '/').length; + if (tests[i].isAbsolute()) { + length++; + } + Path p = resolver.getQPath(tests[i].path); + //System.out.println("Length of " + tests[i].path + " = " + length); + assertEquals("Length of " + tests[i].path + " must reflect " + + "number of elements.", new Integer(length), new Integer(p.getLength())); + } + } + } + + public void testIsNormalized() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid()) { + Path p = resolver.getQPath(tests[i].path); + if (tests[i].isNormalized()) { + assertTrue("Path " + tests[i].path + " must be normalized.", p.isNormalized()); + } else { + assertFalse("Path " + tests[i].path + " must not be normalized.", p.isNormalized()); + } + } + } + } + + public void testGetNameElement() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid()) { + Path p = resolver.getQPath(tests[i].path); + Path.Element nameEl = p.getNameElement(); + Path.Element[] all = p.getElements(); + assertEquals(all[all.length-1], nameEl); + } + } + } + + public void testSubPath() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && tests[i].isNormalized()) { + Path p = resolver.getQPath(tests[i].path); + + // subpath between 0 and length -> equal path + assertEquals(p, p.subPath(0, p.getLength())); + + // subpath a single element + if (p.getLength() > 2) { + Path expected = factory.create(new Path.Element[] {p.getElements()[1]}); + assertEquals(expected, p.subPath(1,2)); + } + // subpath name element + if (p.getLength() > 2) { + Path expected = factory.create(new Path.Element[] {p.getNameElement()}); + assertEquals(expected, p.subPath(p.getLength()-1, p.getLength())); + } + } + } + } + + public void testSubPathInvalid() throws RepositoryException { + Path p = resolver.getQPath("/a/b/c/d/e"); + try { - abs.isAncestorOf(rel); - fail("Path.isAncestorOf(Path) must throw RepositoryException if either path cannot be normalized."); - } catch (RepositoryException e) { - // ok. + p.subPath(2,2); + fail("Path.subPath with identical from/to must throw IllegalArumentException"); + } catch (IllegalArgumentException e) { + // ok } + try { + p.subPath(3,2); + fail("Path.subPath with from > to must throw IllegalArumentException"); + } catch (IllegalArgumentException e) { + // ok + } + try { + p.subPath(-1, 2); + fail("Path.subPath with from == -1 to must throw IllegalArumentException"); + } catch (IllegalArgumentException e) { + // ok + } + try { + p.subPath(1, p.getLength()+1); + fail("Path.subPath with to > length to must throw IllegalArumentException"); + } catch (IllegalArgumentException e) { + // ok + } } -} \ No newline at end of file + + public void testSubPathNotNormalized() throws RepositoryException { + Path root = factory.getRootPath(); + JcrPath[] tests = JcrPath.getTests(); + for (int i = 0; i < tests.length; i++) { + if (tests[i].isValid() && !tests[i].isNormalized()) { + Path p = resolver.getQPath(tests[i].path); + try { + p.subPath(0, p.getLength()); + fail("Path.subPath on a non-normalized path to must throw IllegalArumentException"); + } catch (RepositoryException e) { + // ok + } + } + } + } + + //-------------------------------------------------------------------------- + private static class JcrPathAndAncestor { + + private final String path; + private final String ancestor; + private final int degree; + + private JcrPathAndAncestor(String path, String ancestor, int degree) { + this.path = path; + this.ancestor = ancestor; + this.degree = degree; + } + + private static List list = new ArrayList(); + static { + // normalized + list.add(new JcrPathAndAncestor("abc/def", "abc", 1)); + list.add(new JcrPathAndAncestor("a/b/c/", "a", 2)); + list.add(new JcrPathAndAncestor("prefix:name[2]/prefix:name[2]", "prefix:name[2]/prefix:name[2]", 0)); + list.add(new JcrPathAndAncestor("../../a/b/c/d", "../../a/b/c", 1)); + list.add(new JcrPathAndAncestor("..", "../..", 1)); + list.add(new JcrPathAndAncestor("a/b", ".", 2)); + list.add(new JcrPathAndAncestor("a/b", "..", 3)); + list.add(new JcrPathAndAncestor("a/b", ".", 2)); + list.add(new JcrPathAndAncestor("..", "../..", 1)); + list.add(new JcrPathAndAncestor("../a", "../..", 2)); + list.add(new JcrPathAndAncestor(".", "..", 1)); + list.add(new JcrPathAndAncestor(".", "../..", 2)); + list.add(new JcrPathAndAncestor("../a/b", "../a", 1)); + list.add(new JcrPathAndAncestor("../a/b", "../a", 1)); + list.add(new JcrPathAndAncestor("a", "..", 2)); + list.add(new JcrPathAndAncestor("a", ".", 1)); + list.add(new JcrPathAndAncestor("a", ".", 1)); + list.add(new JcrPathAndAncestor("../a", "..", 1)); + + // not normalized paths + list.add(new JcrPathAndAncestor("a/./b", "a", 1)); + list.add(new JcrPathAndAncestor(".a./.b.", ".a.", 1)); + list.add(new JcrPathAndAncestor("./../.", "./../.", 0)); + list.add(new JcrPathAndAncestor("./../.", "../../..", 2)); + list.add(new JcrPathAndAncestor("a/b/c/../d/..././f", "a", 4)); + list.add(new JcrPathAndAncestor("../a/b/../../../../f", "../a/b/../../../../f", 0)); + list.add(new JcrPathAndAncestor("../a/b/../../../../f", "../../..", 1)); + + list.add(new JcrPathAndAncestor("a/b/c/", "a/b/c/../../..", 3)); + list.add(new JcrPathAndAncestor("a/b/c/", "a/b/c/../../../..", 4)); + list.add(new JcrPathAndAncestor("a/../b", ".", 1)); + list.add(new JcrPathAndAncestor(".", "..", 1)); + list.add(new JcrPathAndAncestor("a/b/../..", "a/b/../..", 0)); + list.add(new JcrPathAndAncestor("a/b/../..", "..", 1)); + list.add(new JcrPathAndAncestor("a", "a", 0)); + list.add(new JcrPathAndAncestor(".../...", "..", 3)); + list.add(new JcrPathAndAncestor("../a/b/../../../../f", "../a/b/../../../../f/../..", 2)); + } + } + + private static class NotAncestor { + + private final String path; + private final String notAncestor; + + private NotAncestor(String path, String notAncestor) { + this.path = path; + this.notAncestor = notAncestor; + } + + private static List list = new ArrayList(); + static { + // false if same path + list.add(new NotAncestor("/", "/")); + list.add(new NotAncestor("/a/.", "/a")); + + // false if siblings or in sibling tree + list.add(new NotAncestor("a", "b")); + list.add(new NotAncestor("a/b", "b")); + list.add(new NotAncestor("../../a/b/c", "../../d/a/b")); + list.add(new NotAncestor("../../a/b/c", "../../d/e/f")); + + // false if path to test is ancestor + list.add(new NotAncestor("/", "/a")); + list.add(new NotAncestor("/", "/a/.")); + list.add(new NotAncestor("/", "/a/b/c")); + list.add(new NotAncestor("a/b", "a/b/c")); + list.add(new NotAncestor("../..", "..")); + + // undefined if ancestor -> false + list.add(new NotAncestor("a", "../a")); + list.add(new NotAncestor("b", "../a")); + list.add(new NotAncestor("../../b", "../../../a")); + list.add(new NotAncestor("../../a", "../../../a")); + list.add(new NotAncestor(".", "../../a")); + list.add(new NotAncestor(".", "../a")); + list.add(new NotAncestor("../a", "../../../a/a")); + list.add(new NotAncestor("../../a/b/c", "../../../a/b")); + list.add(new NotAncestor("../../a/b/c", "../../../a")); + list.add(new NotAncestor("../../d/b/c", "../../../a")); + + // misc relative paths + list.add(new NotAncestor(".", "a/b")); + list.add(new NotAncestor("../..", "..")); + list.add(new NotAncestor("../../a", "..")); + list.add(new NotAncestor("../..", "../a")); + list.add(new NotAncestor(".", ".")); + list.add(new NotAncestor("..", ".")); + list.add(new NotAncestor("../..", ".")); + list.add(new NotAncestor("../../a", "b")); + list.add(new NotAncestor("b", "../../a")); + list.add(new NotAncestor("../../a", ".")); + list.add(new NotAncestor(".", "../../a")); + list.add(new NotAncestor("../../a", "a/..")); + list.add(new NotAncestor("a/..", "../../a")); + list.add(new NotAncestor("../a", ".")); + list.add(new NotAncestor(".", "../a")); + list.add(new NotAncestor("../a", "a/..")); + list.add(new NotAncestor("a/..", "../a")); + list.add(new NotAncestor("../a", "../a/b")); + list.add(new NotAncestor("..", "a")); + list.add(new NotAncestor(".", "a")); + list.add(new NotAncestor("..", "..")); + list.add(new NotAncestor(".", ".")); + list.add(new NotAncestor("..", "../a")); + list.add(new NotAncestor("..", "../../a")); + list.add(new NotAncestor("../../a", "..")); + } + } + + private static class Equivalent { + + private final String path; + private final String other; + private final boolean isEquivalent; + + private Equivalent(String path, String other, boolean isEquivalent) { + this.path = path; + this.other = other; + this.isEquivalent = isEquivalent; + } + + private static List list = new ArrayList(); + static { + list.add(new Equivalent(".", "a/b", false)); + list.add(new Equivalent(".", "a/..", true)); + list.add(new Equivalent(".", ".", true)); + list.add(new Equivalent("..", "..", true)); + list.add(new Equivalent("../..", "../..", true)); + list.add(new Equivalent("../a", "../a/b/..", true)); + list.add(new Equivalent("../a/b/..", "..", false)); + } + } + +} Index: jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/TestAll.java =================================================================== --- jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/TestAll.java (revision 645404) +++ jackrabbit-spi-commons/src/test/java/org/apache/jackrabbit/spi/commons/name/TestAll.java (working copy) @@ -37,6 +37,7 @@ suite.addTestSuite(NameFactoryTest.class); suite.addTestSuite(PathBuilderTest.class); + suite.addTestSuite(PathFactoryTest.class); suite.addTestSuite(PathTest.class); return suite; Index: jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Path.java =================================================================== --- jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Path.java (revision 645404) +++ jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Path.java (working copy) @@ -16,32 +16,96 @@ */ package org.apache.jackrabbit.spi; +import java.io.Serializable; + import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; -import java.io.Serializable; /** * The Path interface defines the qualified representation of - * a JCR path. It consists of {@link Path.Element} objects and is immutable. - * It has the following properties: + * a JCR path. It consists of an ordered list of {@link Path.Element} objects + * and is immutable.

+ * + * A {@link Path.Element} is either {@link Path.Element#denotesName() named} + * or one of the following special elements: + *

+ * + * A Path is defined to have the following characteristics: *

- * isAbsolute():
- * A path is absolute if the first path element denotes the root element '/'. + * + * Equality:
+ * Two paths are equal if they consist of the same elements. *

- * isNormalized():
- * A path is normalized if all '.' and '..' path elements are resolved as much - * as possible. If the path is absolute it is normalized if it contains - * no such elements. For example the path '../../a' is normalized where as - * '../../b/../a/.' is not. Normalized paths never have '.' elements. - * Absolute normalized paths have no and relative normalized paths have no or - * only leading '..' elements. + * + * Length:
+ * The {@link Path#getLength() length} of a path is the number of its elements. *

- * isCanonical():
- * A path is canonical if its absolute and normalized. + * + * Depth:
+ * The {@link Path#getDepth() depth} of a path is + *

+ * The depth of a valid absolute path equals the length of its + * normalization minus 1. *

- * Note, that the implementation must implement the equals method such that - * two Path objects having the equal Elements - * must be equal. + * + * Absolute vs. Relative
+ * A path can be absolute or relative:
+ * A path {@link #isAbsolute() is absolute} if its first element is the root + * element. A path is relative if it is not absolute. + *

+ *

+ * + * Normalization:
+ * A path P {@link Path#isNormalized() is normalized} if P has minimal length + * amongst the set of all paths Q which are equivalent to P.
+ * This means that '.' and '..' elements are resolved as much as possible. + * An absolute path it is normalized if it contains no current nor parent + * element. The normalization of a path is unique.
+ *

+ * + * Equalivalence:
+ * Path P is {@link Path#isEquivalentTo(Path) equivalent} to path Q (in the above sense) + * if the normalization of P is equal to the normalization of Q. This is + * an equivalence relation (i.e. reflexive, transitive, + * and symmetric). + *

+ * + * Canonical Paths:
+ * A path {@link Path#isCanonical() is canonical} if its absolute and normalized. + *

+ * + * Hierarchical Relationship:
+ * The ancestor relationship is a strict partial order (i.e. irreflexive, transitive, + * and asymmetric). Path P is a direct ancestor of path Q if P is equivalent to Q/.. + *
+ * Path P is an {@link Path#isAncestorOf(Path) ancestor} of path Q if + *

+ * + * Path P is an {@link Path#isDescendantOf(Path) descendant} of path Q if + * */ public interface Path extends Serializable { @@ -104,14 +168,10 @@ public boolean isNormalized(); /** - * Returns the normalized path representation of this path. This typically - * involves removing/resolving redundant elements such as "." and ".." from - * the path, e.g. "/a/./b/.." will be normalized to "/a", "../../a/b/c/.." - * will be normalized to "../../a/b", and so on. + * Returns the normalized path representation of this path. *

- * If the normalized path results in an empty path (eg: 'a/..') or if an - * absolute path is normalized that would result in a 'negative' path - * (eg: /a/../../) a MalformedPathException is thrown. + * If the path cannot be normalized (e.g. if an absolute path is normalized + * that would result in a 'negative' path) a RepositoryException is thrown. * * @return a normalized path representation of this path. * @throws RepositoryException if the path cannot be normalized. @@ -120,9 +180,10 @@ public Path getNormalizedPath() throws RepositoryException; /** - * Returns the canonical path representation of this path. This typically - * involves removing/resolving redundant elements such as "." and ".." from - * the path. + * Returns the canonical path representation of this path. + *

+ * If the path is relative or cannot be normalized a RepositoryException + * is thrown. * * @return a canonical path representation of this path. * @throws RepositoryException if this path can not be canonicalized @@ -143,7 +204,8 @@ public Path computeRelativePath(Path other) throws RepositoryException; /** - * Returns the ancestor path of the specified relative degree. + * Normalizes this path and returns the ancestor path of the specified + * relative degree. *

* An ancestor of relative degree x is the path that is x * levels up along the path. @@ -155,26 +217,29 @@ * of this path, which returns the root path. * *

- * Note that there migth be an unexpected result if this path is not - * normalized, e.g. the ancestor of degree = 1 of the path "../.." would - * be ".." although this is not the parent of "../..". + * If this path is relative the implementation may not be able to determine + * if the ancestor at degree exists. Such an implementation + * should properly build the ancestor (i.e. parent of .. is ../..) and + * leave if it the caller to throw PathNotFoundException. * * @param degree the relative degree of the requested ancestor. - * @return the ancestor path of the specified degree. - * @throws PathNotFoundException if there is no ancestor of the specified degree. + * @return the normalized ancestor path of the specified degree. * @throws IllegalArgumentException if degree is negative. + * @throws PathNotFoundException if the implementation is able to determine + * that there is no ancestor of the specified degree. In case of this + * being an absolute path, this would be the case if degree is + * greater that the {@link #getDepth() depth} of this path. */ public Path getAncestor(int degree) throws IllegalArgumentException, PathNotFoundException; /** * Returns the number of ancestors of this path. This is the equivalent - * of {@link #getDepth()} - 1. - *

- * Note that the returned value might be negative if this path is not - * canonical, e.g. the depth of "../../a" is -1, its ancestor count is - * therefore -2. + * of {@link #getDepth()} in case of a absolute path. + * For relative path the number of ancestors cannot be determined and + * -1 should be returned. * - * @return the number of ancestors of this path + * @return the number of ancestors of this path or -1 if the number of + * ancestors cannot be determined. * @see #getDepth() * @see #getLength() * @see #isCanonical() @@ -201,7 +266,8 @@ * Returns the depth of this path. The depth reflects the absolute or * relative hierarchy level this path is representing, depending on whether * this path is an absolute or a relative path. The depth also takes '.' - * and '..' elements into account. + * and '..' elements into account. The depth of the root path and the + * current path must be 0. *

* Note that the returned value might be negative if this path is not * canonical, e.g. the depth of "../../a" is -1. @@ -213,9 +279,23 @@ public int getDepth(); /** + * Determines if the the other path would be equal to this + * path if both of them are normalized. + * + * @param other Another path. + * @return true if the given other path is equivalent to this path. + * @throws IllegalArgumentException if the given path is null + * or if not both paths are either absolute or relative. + * @throws RepositoryException if any of the path cannot be normalized. + */ + public boolean isEquivalentTo(Path other) throws IllegalArgumentException, RepositoryException; + + /** * Determines if this path is an ancestor of the specified path, * based on their (absolute or relative) hierarchy level as returned by - * {@link #getDepth()}. + * {@link #getDepth()}. In case of undefined ancestor/descendant + * relationship that might occur with relative paths, the return value + * should be false. * * @return true if other is a descendant; * otherwise false. @@ -229,14 +309,16 @@ /** * Determines if this path is a descendant of the specified path, * based on their (absolute or relative) hierarchy level as returned by - * {@link #getDepth()}. + * {@link #getDepth()}. In case of undefined ancestor/descendant + * relationship that might occur with relative paths, the return value + * should be false. * * @return true if other is an ancestor; - * otherwise false + * otherwise false. * @throws IllegalArgumentException if the given path is null * or if not both paths are either absolute or relative. * @throws RepositoryException if any of the path cannot be normalized. - * @see #getDepth() + * @see #isAncestorOf(Path) */ public boolean isDescendantOf(Path other) throws IllegalArgumentException, RepositoryException; @@ -248,10 +330,16 @@ * out of the possible range. A RepositoryException is thrown * if this Path is not normalized. * - * @param from - * @param to - * @return - * @throws IllegalArgumentException + * @param from index of the element to start with and low endpoint + * (inclusive) within the list of elements to use for the sub-path. + * @param to index of the element outside of the range i.e. high endpoint + * (exclusive) within the list of elements to use for the sub-path. + * @return a new Path consisting of those Path.Element objects + * between the given from, inclusive, and the given + * to, exclusive. + * @throws IllegalArgumentException if from + * is greater or equal than to or if any of both params is + * out of the possible range. * @throws RepositoryException If this Path is not normalized. */ public Path subPath(int from, int to) throws IllegalArgumentException, RepositoryException; @@ -277,7 +365,7 @@ * its elements separated by {@link Path#DELIMITER}. * * @see Path.Element#getString() - * @return + * @return Returns the String representation of this Path. */ public String getString(); @@ -359,12 +447,12 @@ public boolean denotesName(); /** - * Return the String presentation of a{@link Path.Element}. It must be + * Return the String presentation of a {@link Path.Element}. It must be * in the format "{namespaceURI}localPart" or * "{namespaceURI}localPart[index]" in case of an index * greater than {@link Path#INDEX_DEFAULT}. * - * @return + * @return String representation of a {@link Path.Element}. */ public String getString(); }