You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 10:17:28 UTC

[sling-org-apache-sling-testing-jcr-mock] 01/10: SLING-4042 Donate sling-mock, jcr-mock, osgi-mock implementation

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.testing.jcr-mock-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-jcr-mock.git

commit 39572249976327b490288b8f876908d1e49cbd1f
Author: sseifert <ss...@unknown>
AuthorDate: Mon Oct 13 11:54:39 2014 +0000

    SLING-4042 Donate sling-mock, jcr-mock, osgi-mock implementation
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/testing/jcr-mock@1631356 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  91 ++++
 .../sling/testing/mock/jcr/AbstractItem.java       | 129 ++++++
 .../apache/sling/testing/mock/jcr/ItemFilter.java  |  31 ++
 .../org/apache/sling/testing/mock/jcr/MockJcr.java |  62 +++
 .../testing/mock/jcr/MockNamespaceRegistry.java    |  72 +++
 .../apache/sling/testing/mock/jcr/MockNode.java    | 499 +++++++++++++++++++++
 .../sling/testing/mock/jcr/MockNodeType.java       | 155 +++++++
 .../testing/mock/jcr/MockNodeTypeManager.java      | 103 +++++
 .../sling/testing/mock/jcr/MockNodeTypes.java      |  49 ++
 .../testing/mock/jcr/MockObservationManager.java   |  65 +++
 .../sling/testing/mock/jcr/MockProperty.java       | 306 +++++++++++++
 .../sling/testing/mock/jcr/MockRepository.java     |  85 ++++
 .../apache/sling/testing/mock/jcr/MockSession.java | 354 +++++++++++++++
 .../sling/testing/mock/jcr/MockWorkspace.java      | 149 ++++++
 .../sling/testing/mock/jcr/ResourceUtil.java       | 219 +++++++++
 .../sling/testing/mock/jcr/package-info.java       |  23 +
 src/site/markdown/index.md                         |  38 ++
 src/site/markdown/usage.md                         |  17 +
 .../sling/testing/mock/jcr/AbstractItemTest.java   |  95 ++++
 .../sling/testing/mock/jcr/MockNodeTest.java       | 129 ++++++
 .../sling/testing/mock/jcr/MockPropertyTest.java   | 240 ++++++++++
 .../sling/testing/mock/jcr/MockRepositoryTest.java |  63 +++
 .../sling/testing/mock/jcr/MockSessionTest.java    | 228 ++++++++++
 .../sling/testing/mock/jcr/MockWorkspaceTest.java  |  64 +++
 .../sling/testing/mock/jcr/ResourceUtilTest.java   | 221 +++++++++
 25 files changed, 3487 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6bd600b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>22</version>
+        <relativePath>../../parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.sling.testing.jcr-mock</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>Apache Sling Testing JCR Mock</name>
+    <description>Mock implementation of selected JCR APIs.</description>
+
+    <properties>
+        <sling.java.version>6</sling.java.version>
+    </properties>
+
+    <dependencies>
+    
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <scope>compile</scope>
+        </dependency>
+    
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-jcr-commons</artifactId>
+            <version>2.8.0</version>
+            <scope>compile</scope>
+        </dependency>
+    
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>15.0</version>
+            <scope>compile</scope>
+        </dependency>
+    
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.0.1</version>
+            <scope>compile</scope>
+        </dependency>
+    
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+            <scope>test</scope>
+        </dependency>  
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.9.5</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+  
+    </dependencies>
+  
+</project>
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/AbstractItem.java b/src/main/java/org/apache/sling/testing/mock/jcr/AbstractItem.java
new file mode 100644
index 0000000..0eb92f3
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/AbstractItem.java
@@ -0,0 +1,129 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.ItemVisitor;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Mock {@link Item} implementation.
+ */
+abstract class AbstractItem implements Item {
+
+    private final String path;
+    private final Session session;
+
+    public AbstractItem(final String path, final Session session) {
+        this.path = path;
+        this.session = session;
+    }
+
+    @Override
+    public String getName() {
+        return ResourceUtil.getName(this.path);
+    }
+
+    @Override
+    public String getPath() {
+        return this.path;
+    }
+
+    @Override
+    public Node getParent() throws RepositoryException {
+        return (Node) getSession().getItem(ResourceUtil.getParent(this.path));
+    }
+
+    @Override
+    public Session getSession() {
+        return this.session;
+    }
+
+    @Override
+    public boolean isModified() {
+        return false;
+    }
+
+    @Override
+    public boolean isNew() {
+        return false;
+    }
+
+    @Override
+    public Item getAncestor(final int depth) throws RepositoryException {
+        if (depth < 0 || depth > getDepth()) {
+            throw new ItemNotFoundException();
+        }
+        return this.session.getItem(ResourceUtil.getParent(this.path, depth));
+    }
+
+    protected String makeAbsolutePath(final String relativePath) {
+        String absolutePath = relativePath;
+        // ensure the path is absolute and normalized
+        if (!StringUtils.startsWith(absolutePath, "/")) {
+            absolutePath = this.path + "/" + absolutePath; // NOPMD
+        }
+        return ResourceUtil.normalize(absolutePath);
+    }
+
+    protected MockSession getMockedSession() {
+        return (MockSession) this.session;
+    }
+
+    @Override
+    public void remove() throws RepositoryException {
+        getSession().removeItem(getPath());
+    }
+
+    @Override
+    public int getDepth() throws RepositoryException {
+        if (StringUtils.equals("/", this.path)) {
+            return 0;
+        } else {
+            return StringUtils.countMatches(this.path, "/");
+        }
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public void accept(final ItemVisitor visitor) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSame(final Item otherItem) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void refresh(final boolean keepChanges) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void save() {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/ItemFilter.java b/src/main/java/org/apache/sling/testing/mock/jcr/ItemFilter.java
new file mode 100644
index 0000000..2e144a1
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/ItemFilter.java
@@ -0,0 +1,31 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+/**
+ * Used internally for filtering items in JCR data map.
+ */
+interface ItemFilter {
+
+    boolean accept(Item item) throws RepositoryException;
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockJcr.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockJcr.java
new file mode 100644
index 0000000..11c3c1d
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockJcr.java
@@ -0,0 +1,62 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+/**
+ * Factory for mock JCR objects.
+ */
+public final class MockJcr {
+
+    /**
+     * Default workspace name
+     */
+    public static final String DEFAULT_WORKSPACE = "mockedWorkspace";
+
+    private MockJcr() {
+        // static methods only
+    }
+
+    /**
+     * Create a new mocked in-memory JCR repository. Beware: each session has
+     * its own data store.
+     * @return JCR repository
+     */
+    public static Repository newRepository() {
+        return new MockRepository();
+    }
+
+    /**
+     * Create a new mocked in-memory JCR session. It contains only the root
+     * node. All data of the session is thrown away if it gets garbage
+     * collected.
+     * @return JCR session
+     */
+    public static Session newSession() {
+        try {
+            return newRepository().login();
+        } catch (RepositoryException ex) {
+            throw new RuntimeException("Creating mocked JCR session failed.", ex);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockNamespaceRegistry.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockNamespaceRegistry.java
new file mode 100644
index 0000000..91b194f
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockNamespaceRegistry.java
@@ -0,0 +1,72 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import java.util.Set;
+
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.RepositoryException;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+
+/**
+ * Mock {@link NamespaceRegistry} implementation.
+ */
+class MockNamespaceRegistry implements NamespaceRegistry {
+
+    private final BiMap<String, String> namespacePrefixMapping = HashBiMap.create();
+
+    public MockNamespaceRegistry() {
+        this.namespacePrefixMapping.put("jcr", "http://www.jcp.org/jcr/1.0");
+    }
+
+    @Override
+    public String getURI(final String prefix) {
+        return this.namespacePrefixMapping.get(prefix);
+    }
+
+    @Override
+    public String getPrefix(final String uri) {
+        return this.namespacePrefixMapping.inverse().get(uri);
+    }
+
+    @Override
+    public void registerNamespace(final String prefix, final String uri) {
+        this.namespacePrefixMapping.put(prefix, uri);
+    }
+
+    @Override
+    public void unregisterNamespace(final String prefix) {
+        this.namespacePrefixMapping.remove(prefix);
+    }
+
+    @Override
+    public String[] getPrefixes() throws RepositoryException {
+        Set<String> keys = this.namespacePrefixMapping.keySet();
+        return keys.toArray(new String[keys.size()]);
+    }
+
+    @Override
+    public String[] getURIs() throws RepositoryException {
+        Set<String> values = this.namespacePrefixMapping.values();
+        return values.toArray(new String[values.size()]);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java
new file mode 100644
index 0000000..41350d6
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockNode.java
@@ -0,0 +1,499 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+import javax.jcr.Binary;
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RangeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.lock.Lock;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter;
+import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter;
+
+/**
+ * Mock {@link Node} implementation
+ */
+class MockNode extends AbstractItem implements Node {
+
+    private final UUID uuid = UUID.randomUUID();
+    private final NodeType nodeType;
+
+    public MockNode(final String path, final Session session, final NodeType nodeType) {
+        super(path, session);
+        this.nodeType = nodeType;
+    }
+
+    @Override
+    public Node addNode(final String relPath) throws RepositoryException {
+        return addNode(relPath, JcrConstants.NT_UNSTRUCTURED);
+    }
+
+    @Override
+    public Node addNode(final String relPath, final String primaryNodeTypeName) throws RepositoryException {
+        String path = makeAbsolutePath(relPath);
+        Node node = new MockNode(path, getSession(), new MockNodeType(primaryNodeTypeName));
+        getMockedSession().addItem(node);
+        return node;
+    }
+
+    @Override
+    public Node getNode(final String relPath) throws RepositoryException {
+        String path = makeAbsolutePath(relPath);
+        return getSession().getNode(path);
+    }
+
+    @Override
+    public NodeIterator getNodes() throws RepositoryException {
+        RangeIterator items = getMockedSession().listChildren(getPath(), new ItemFilter() {
+            @Override
+            public boolean accept(final Item item) {
+                return item instanceof Node;
+            }
+        });
+        return new NodeIteratorAdapter(items, items.getSize());
+    }
+
+    @Override
+    public NodeIterator getNodes(final String namePattern) throws RepositoryException {
+        final Pattern pattern = Pattern.compile(namePattern);
+        RangeIterator items = getMockedSession().listChildren(getPath(), new ItemFilter() {
+            @Override
+            public boolean accept(final Item item) throws RepositoryException {
+                return (item instanceof Node) && pattern.matcher(item.getName()).matches();
+            }
+        });
+        return new NodeIteratorAdapter(items, items.getSize());
+    }
+
+    @Override
+    public PropertyIterator getProperties() throws RepositoryException {
+        RangeIterator items = getMockedSession().listChildren(getPath(), new ItemFilter() {
+            @Override
+            public boolean accept(final Item item) {
+                return item instanceof Property;
+            }
+        });
+        return new PropertyIteratorAdapter(items, items.getSize());
+    }
+
+    @Override
+    public PropertyIterator getProperties(final String namePattern) throws RepositoryException {
+        final Pattern pattern = Pattern.compile(namePattern);
+        RangeIterator items = getMockedSession().listChildren(getPath(), new ItemFilter() {
+            @Override
+            public boolean accept(final Item item) throws RepositoryException {
+                return (item instanceof Property) && pattern.matcher(item.getName()).matches();
+            }
+        });
+        return new PropertyIteratorAdapter(items, items.getSize());
+    }
+
+    @Override
+    public Property getProperty(final String relPath) throws RepositoryException {
+        String path = makeAbsolutePath(relPath);
+        return getSession().getProperty(path);
+    }
+
+    @Override
+    public String getIdentifier() {
+        return this.uuid.toString();
+    }
+
+    @Override
+    public String getUUID() {
+        return getIdentifier();
+    }
+
+    @Override
+    public boolean hasNode(final String relPath) throws RepositoryException {
+        String path = makeAbsolutePath(relPath);
+        return getSession().nodeExists(path);
+    }
+
+    @Override
+    public boolean hasNodes() throws RepositoryException {
+        return getNodes().hasNext();
+    }
+
+    @Override
+    public boolean hasProperties() throws RepositoryException {
+        return getProperties().hasNext();
+    }
+
+    @Override
+    public boolean hasProperty(final String relPath) throws RepositoryException {
+        String path = makeAbsolutePath(relPath);
+        return getSession().propertyExists(path);
+    }
+
+    @Override
+    public Property setProperty(final String name, final Value value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final Value[] values) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(values);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final String[] values) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(values);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final String value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public Property setProperty(final String name, final InputStream value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final boolean value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final double value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final long value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final Calendar value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final Node value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final Binary value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public Property setProperty(final String name, final BigDecimal value) throws RepositoryException {
+        Property property = new MockProperty(getPath() + "/" + name, getSession());
+        property.setValue(value);
+        getMockedSession().addItem(property);
+        return property;
+    }
+
+    @Override
+    public boolean isNode() {
+        return true;
+    }
+
+    @Override
+    public boolean isNodeType(final String nodeTypeName) throws RepositoryException {
+        return this.nodeType.isNodeType(nodeTypeName);
+    }
+
+    @Override
+    public NodeType getPrimaryNodeType() {
+        return this.nodeType;
+    }
+
+    @Override
+    public Item getPrimaryItem() throws RepositoryException {
+        // support "jcr:content" node and "jcr:data" property as primary items
+        if (hasProperty(JcrConstants.JCR_DATA)) {
+            return getProperty(JcrConstants.JCR_DATA);
+        } else if (hasNode(JcrConstants.JCR_CONTENT)) {
+            return getNode(JcrConstants.JCR_CONTENT);
+        } else {
+            throw new ItemNotFoundException();
+        }
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public Property setProperty(final String name, final Value value, final int type) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(final String name, final Value[] values, final int type) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(final String name, final String[] values, final int type) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property setProperty(final String name, final String value, final int type) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addMixin(final String pMixinName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canAddMixin(final String pMixinName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void cancelMerge(final Version pVersion) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Version checkin() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void checkout() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void doneMerge(final Version pVersion) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Version getBaseVersion() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getCorrespondingNodePath(final String workspaceName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinition getDefinition() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getIndex() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Lock getLock() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType[] getMixinNodeTypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getReferences() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public VersionHistory getVersionHistory() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean holdsLock() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isCheckedOut() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isLocked() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Lock lock(final boolean isDeep, final boolean isSessionScoped) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator merge(final String srcWorkspace, final boolean bestEffort) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void orderBefore(final String srcChildRelPath, final String destChildRelPath) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeMixin(final String mixinName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(final String versionName, final boolean removeExisting) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(final Version version, final boolean removeExisting) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(final Version version, final String relPath, final boolean removeExisting) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restoreByLabel(final String versionLabel, final boolean removeExisting) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void unlock() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void update(final String srcWorkspaceName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void followLifecycleTransition(final String transition) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getAllowedLifecycleTransistions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator getNodes(final String[] nameGlobs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getProperties(final String[] nameGlobs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getReferences(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeIterator getSharedSet() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getWeakReferences() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyIterator getWeakReferences(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeShare() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeSharedSet() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setPrimaryType(final String pNodeTypeName) {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeType.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeType.java
new file mode 100644
index 0000000..7033df5
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeType.java
@@ -0,0 +1,155 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeIterator;
+import javax.jcr.nodetype.PropertyDefinition;
+
+/**
+ * Mock {@link NodeType} implementation.
+ */
+class MockNodeType implements NodeType {
+
+    private final String name;
+
+    public MockNodeType(final String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getName() {
+        return this.name;
+    }
+
+    @Override
+    public boolean isNodeType(final String nodeTypeName) {
+        // node type inheritance not supported
+        return this.name.equals(nodeTypeName);
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public boolean canAddChildNode(final String childNodeName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canAddChildNode(final String childNodeName, final String nodeTypeName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canRemoveItem(final String itemName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canSetProperty(final String propertyName, final Value value) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canSetProperty(final String propertyName, final Value[] values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinition[] getChildNodeDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinition[] getDeclaredChildNodeDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyDefinition[] getDeclaredPropertyDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType[] getDeclaredSupertypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getPrimaryItemName() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyDefinition[] getPropertyDefinitions() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType[] getSupertypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasOrderableChildNodes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isMixin() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canRemoveNode(final String nodeName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean canRemoveProperty(final String propertyName) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator getDeclaredSubtypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator getSubtypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getDeclaredSupertypeNames() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isAbstract() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isQueryable() {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeTypeManager.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeTypeManager.java
new file mode 100644
index 0000000..edec788
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeTypeManager.java
@@ -0,0 +1,103 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.nodetype.NodeDefinitionTemplate;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeDefinition;
+import javax.jcr.nodetype.NodeTypeIterator;
+import javax.jcr.nodetype.NodeTypeManager;
+import javax.jcr.nodetype.NodeTypeTemplate;
+import javax.jcr.nodetype.PropertyDefinitionTemplate;
+
+/**
+ * Mock {@link NodeTypeManager} implementation.
+ */
+class MockNodeTypeManager implements NodeTypeManager {
+
+    @Override
+    public NodeType getNodeType(String nodeTypeName) {
+        // accept all node types and return a mock
+        return new MockNodeType(nodeTypeName);
+    }
+
+    @Override
+    public boolean hasNodeType(String name) {
+        // accept all node types
+        return true;
+    }
+
+    // --- unsupported operations ---
+
+    @Override
+    public NodeTypeIterator getAllNodeTypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator getPrimaryNodeTypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator getMixinNodeTypes() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeTemplate createNodeTypeTemplate() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeTemplate createNodeTypeTemplate(NodeTypeDefinition ntd) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeDefinitionTemplate createNodeDefinitionTemplate() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public PropertyDefinitionTemplate createPropertyDefinitionTemplate() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType registerNodeType(NodeTypeDefinition ntd, boolean allowUpdate) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeTypeIterator registerNodeTypes(NodeTypeDefinition[] ntds, boolean allowUpdate) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void unregisterNodeType(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void unregisterNodeTypes(String[] names) {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeTypes.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeTypes.java
new file mode 100644
index 0000000..1b081b6
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockNodeTypes.java
@@ -0,0 +1,49 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.jackrabbit.JcrConstants;
+
+/**
+ * Collection of mocked node type instances.
+ */
+public final class MockNodeTypes {
+
+    private MockNodeTypes() {
+        // constants only
+    }
+
+    /**
+     * Node type NT_UNSTRUCTURED
+     */
+    public static final NodeType NT_UNSTRUCTURED = new MockNodeType(JcrConstants.NT_UNSTRUCTURED);
+
+    /**
+     * Node type NT_FOLDER
+     */
+    public static final NodeType NT_FOLDER = new MockNodeType(JcrConstants.NT_FOLDER);
+
+    /**
+     * Node type NT_FILE
+     */
+    public static final NodeType NT_FILE = new MockNodeType(JcrConstants.NT_FILE);
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockObservationManager.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockObservationManager.java
new file mode 100644
index 0000000..4988fa0
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockObservationManager.java
@@ -0,0 +1,65 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.observation.EventJournal;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.EventListenerIterator;
+import javax.jcr.observation.ObservationManager;
+
+/**
+ * Mock {@link ObservationManager} implementation.
+ */
+class MockObservationManager implements ObservationManager {
+
+    @Override
+    public void addEventListener(final EventListener listener, final int eventTypes, final String absPath,
+            final boolean isDeep, final String[] uuid, final String[] nodeTypeName, final boolean noLocal) {
+        // do nothing
+    }
+
+    @Override
+    public void removeEventListener(final EventListener listener) {
+        // do nothing
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public EventListenerIterator getRegisteredEventListeners() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setUserData(final String userData) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public EventJournal getEventJournal() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public EventJournal getEventJournal(final int eventTypes, final String absPath, final boolean isDeep,
+            final String[] uuid, final String[] nodeTypeName) {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java
new file mode 100644
index 0000000..468a840
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockProperty.java
@@ -0,0 +1,306 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
+
+import org.apache.jackrabbit.value.BinaryValue;
+
+/**
+ * Mock {@link Property} implementation
+ */
+class MockProperty extends AbstractItem implements Property {
+
+    private Value[] values;
+    private boolean isMultiple;
+
+    public MockProperty(final String path, final Session session) throws RepositoryException {
+        super(path, session);
+        this.values = new Value[] { getSession().getValueFactory().createValue("") };
+    }
+
+    private Value internalGetValue() throws ValueFormatException {
+        if (this.values.length > 1) {
+            throw new ValueFormatException(this
+                    + " is a multi-valued property, so it's values can only be retrieved as an array");
+        } else {
+            return this.values[0];
+        }
+    }
+
+    @Override
+    public Value getValue() throws ValueFormatException {
+        return internalGetValue();
+    }
+
+    @Override
+    public Value[] getValues() {
+        Value[] valuesCopy = new Value[this.values.length];
+        for (int i = 0; i < this.values.length; i++) {
+            valuesCopy[i] = this.values[i];
+        }
+        return valuesCopy;
+    }
+
+    @Override
+    public void setValue(final Value newValue) {
+        this.values = new Value[] { newValue };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final Value[] newValues) {
+        this.values = new Value[newValues.length];
+        for (int i = 0; i < newValues.length; i++) {
+            this.values[i] = newValues[i];
+        }
+        this.isMultiple = true;
+    }
+
+    @Override
+    public void setValue(final String newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final String[] newValues) throws RepositoryException {
+        this.values = new Value[newValues.length];
+        for (int i = 0; i < newValues.length; i++) {
+            this.values[i] = getSession().getValueFactory().createValue(newValues[i]);
+        }
+        this.isMultiple = true;
+    }
+
+    @Override
+    public void setValue(final InputStream newValue) throws RepositoryException {
+        this.values = new Value[] { new BinaryValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final long newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final double newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final Calendar newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final boolean newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final Node newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final Binary newValue) throws RepositoryException {
+        this.values = new Value[] { new BinaryValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public void setValue(final BigDecimal newValue) throws RepositoryException {
+        this.values = new Value[] { getSession().getValueFactory().createValue(newValue) };
+        this.isMultiple = false;
+    }
+
+    @Override
+    public boolean getBoolean() throws RepositoryException {
+        return internalGetValue().getBoolean();
+    }
+
+    @Override
+    public Calendar getDate() throws RepositoryException {
+        return internalGetValue().getDate();
+    }
+
+    @Override
+    public double getDouble() throws RepositoryException {
+        return internalGetValue().getDouble();
+    }
+
+    @Override
+    public long getLong() throws RepositoryException {
+        return internalGetValue().getLong();
+    }
+
+    @Override
+    public String getString() throws RepositoryException {
+        return internalGetValue().getString();
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public InputStream getStream() throws RepositoryException {
+        return internalGetValue().getStream();
+    }
+
+    @Override
+    public Binary getBinary() throws RepositoryException {
+        return internalGetValue().getBinary();
+    }
+
+    @Override
+    public BigDecimal getDecimal() throws RepositoryException {
+        return internalGetValue().getDecimal();
+    }
+
+    @Override
+    public int getType() throws RepositoryException {
+        return this.values[0].getType();
+    }
+
+    @Override
+    public long getLength() throws RepositoryException {
+        return getValue().getString().length();
+    }
+
+    @Override
+    public long[] getLengths() throws RepositoryException {
+        long[] lengths = new long[this.values.length];
+        for (int i = 0; i < this.values.length; i++) {
+            lengths[i] = this.values[i].getString().length();
+        }
+        return lengths;
+    }
+
+    @Override
+    public boolean isNode() {
+        return false;
+    }
+
+    @Override
+    public boolean isMultiple() {
+        return this.isMultiple;
+    }
+
+    @Override
+    public PropertyDefinition getDefinition() {
+        return new MockPropertyDefinition();
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public Node getNode() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property getProperty() {
+        throw new UnsupportedOperationException();
+    }
+
+    private final class MockPropertyDefinition implements PropertyDefinition {
+
+        @Override
+        public boolean isMultiple() {
+            return MockProperty.this.isMultiple();
+        }
+
+        // --- unsupported operations ---
+        @Override
+        public Value[] getDefaultValues() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getRequiredType() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String[] getValueConstraints() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public NodeType getDeclaringNodeType() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String getName() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getOnParentVersion() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isAutoCreated() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isMandatory() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isProtected() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public String[] getAvailableQueryOperators() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isFullTextSearchable() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isQueryOrderable() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockRepository.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockRepository.java
new file mode 100644
index 0000000..324820e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockRepository.java
@@ -0,0 +1,85 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import javax.jcr.Credentials;
+import javax.jcr.Repository;
+import javax.jcr.Session;
+import javax.jcr.Value;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * Mock {@link Repository} implementation. The data is stored inside the mocked
+ * session, not the repository - so it is not possible to open multiple session
+ * to access the same data in this mock implementation.
+ */
+class MockRepository implements Repository {
+
+    @Override
+    public Session login() {
+        return new MockSession(this);
+    }
+
+    @Override
+    public Session login(final String workspaceName) {
+        return login();
+    }
+
+    @Override
+    public Session login(final Credentials credentials) {
+        return login();
+    }
+
+    @Override
+    public Session login(final Credentials credentials, final String workspaceName) {
+        return login();
+    }
+
+    @Override
+    public String[] getDescriptorKeys() {
+        return ArrayUtils.EMPTY_STRING_ARRAY;
+    }
+
+    @Override
+    public boolean isStandardDescriptor(final String key) {
+        return false;
+    }
+
+    @Override
+    public boolean isSingleValueDescriptor(final String key) {
+        return false;
+    }
+
+    @Override
+    public Value getDescriptorValue(final String key) {
+        return null;
+    }
+
+    @Override
+    public Value[] getDescriptorValues(final String key) { // NOPMD
+        return null; // NOPMD
+    }
+
+    @Override
+    public String getDescriptor(final String key) {
+        return null;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java
new file mode 100644
index 0000000..5567318
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java
@@ -0,0 +1,354 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import javax.jcr.Credentials;
+import javax.jcr.Item;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.RangeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.ValueFactory;
+import javax.jcr.Workspace;
+import javax.jcr.retention.RetentionManager;
+import javax.jcr.security.AccessControlManager;
+
+import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.xml.sax.ContentHandler;
+
+/**
+ * Mock {@link Session} implementation. This instance holds the JCR data in a
+ * simple ordered map.
+ */
+class MockSession implements Session {
+
+    private final Repository repository;
+    private final Workspace workspace;
+
+    // Use linked hashmap to ensure ordering when adding items is preserved.
+    private final Map<String, Item> items = new LinkedHashMap<String, Item>();
+
+    public MockSession(final Repository repository) {
+        this.repository = repository;
+        this.workspace = new MockWorkspace(this);
+        this.items.put("/", new MockNode("/", this, MockNodeTypes.NT_UNSTRUCTURED));
+    }
+
+    @Override
+    public ValueFactory getValueFactory() {
+        return ValueFactoryImpl.getInstance();
+    }
+
+    @Override
+    public Item getItem(final String absPath) throws RepositoryException {
+        Item item = this.items.get(absPath);
+        if (item != null) {
+            return item;
+        } else {
+            throw new PathNotFoundException(String.format("No item found at: %s.", absPath));
+        }
+    }
+
+    @Override
+    public Node getNode(final String absPath) throws RepositoryException {
+        Item item = getItem(absPath);
+        if (item instanceof Node) {
+            return (Node) item;
+        } else {
+            throw new PathNotFoundException(String.format("No node found at: %s.", absPath));
+        }
+    }
+
+    @Override
+    public Node getNodeByIdentifier(final String id) throws RepositoryException {
+        for (Item item : this.items.values()) {
+            if (item instanceof Node) {
+                Node node = (Node) item;
+                if (node.getIdentifier().equals(id)) {
+                    return node;
+                }
+            }
+        }
+        throw new ItemNotFoundException(String.format("No node found with id: %s.", id));
+    }
+
+    @Override
+    public Property getProperty(final String absPath) throws RepositoryException {
+        Item item = getItem(absPath);
+        if (item instanceof Property) {
+            return (Property) item;
+        } else {
+            throw new PathNotFoundException(String.format("No property found at: %s.", absPath));
+        }
+    }
+
+    @Override
+    public boolean nodeExists(final String absPath) throws RepositoryException {
+        try {
+            getNode(absPath);
+            return true;
+        } catch (PathNotFoundException ex) {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean propertyExists(final String absPath) throws RepositoryException {
+        try {
+            getProperty(absPath);
+            return true;
+        } catch (PathNotFoundException ex) {
+            return false;
+        }
+    }
+
+    @Override
+    public void removeItem(final String absPath) {
+        removeItemWithChildren(absPath);
+    }
+
+    @Override
+    public Node getRootNode() {
+        return (Node) this.items.get("/");
+    }
+
+    @Override
+    public Node getNodeByUUID(final String uuid) throws RepositoryException {
+        return getNodeByIdentifier(uuid);
+    }
+
+    /**
+     * Add item
+     * @param item item
+     * @throws RepositoryException
+     */
+    void addItem(final Item item) throws RepositoryException {
+        this.items.put(item.getPath(), item);
+    }
+
+    /**
+     * Remove item incl. children
+     * @param path Item path
+     */
+    void removeItemWithChildren(final String path) {
+        List<String> pathsToRemove = new ArrayList<String>();
+
+        // build regex pattern for node and all its children
+        Pattern pattern = Pattern.compile("^" + Pattern.quote(path) + "(/.+)?$");
+
+        for (String itemPath : this.items.keySet()) {
+            if (pattern.matcher(itemPath).matches()) {
+                pathsToRemove.add(itemPath);
+            }
+        }
+        for (String pathToRemove : pathsToRemove) {
+            this.items.remove(pathToRemove);
+        }
+    }
+
+    RangeIterator listChildren(final String parentPath, final ItemFilter filter) throws RepositoryException {
+        List<Item> children = new ArrayList<Item>();
+
+        // build regex pattern for all child paths of parent
+        Pattern pattern = Pattern.compile("^" + Pattern.quote(parentPath) + "/[^/]+$");
+
+        // collect child resources
+        for (Item item : this.items.values()) {
+            if (pattern.matcher(item.getPath()).matches() && (filter == null || filter.accept(item))) {
+                children.add(item);
+            }
+        }
+
+        return new RangeIteratorAdapter(children.iterator(), children.size());
+    }
+
+    @Override
+    public boolean hasPendingChanges() {
+        return false;
+    }
+
+    @Override
+    public boolean itemExists(final String absPath) {
+        return this.items.get(absPath) != null;
+    }
+
+    @Override
+    public Workspace getWorkspace() {
+        return this.workspace;
+    }
+
+    @Override
+    public String getUserID() {
+        return "mockedUserId";
+    }
+
+    @Override
+    public String getNamespacePrefix(final String uri) throws RepositoryException {
+        return getWorkspace().getNamespaceRegistry().getPrefix(uri);
+    }
+
+    @Override
+    public String[] getNamespacePrefixes() throws RepositoryException {
+        return getWorkspace().getNamespaceRegistry().getPrefixes();
+    }
+
+    @Override
+    public String getNamespaceURI(final String prefix) throws RepositoryException {
+        return getWorkspace().getNamespaceRegistry().getURI(prefix);
+    }
+
+    @Override
+    public void setNamespacePrefix(final String prefix, final String uri) throws RepositoryException {
+        getWorkspace().getNamespaceRegistry().registerNamespace(prefix, uri);
+    }
+
+    @Override
+    public Repository getRepository() {
+        return this.repository;
+    }
+
+    @Override
+    public void save() {
+        // do nothing
+    }
+
+    @Override
+    public void refresh(final boolean keepChanges) throws RepositoryException {
+        // do nothing
+    }
+
+    @Override
+    public void checkPermission(final String absPath, final String actions) {
+        // always grant permission
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public void addLockToken(final String lt) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void exportDocumentView(final String absPath, final ContentHandler contentHandler, final boolean skipBinary,
+            final boolean noRecurse) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void exportDocumentView(final String absPath, final OutputStream out, final boolean skipBinary,
+            final boolean noRecurse) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void exportSystemView(final String absPath, final ContentHandler contentHandler, final boolean skipBinary,
+            final boolean noRecurse) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void exportSystemView(final String absPath, final OutputStream out, final boolean skipBinary,
+            final boolean noRecurse) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getAttribute(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getAttributeNames() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ContentHandler getImportContentHandler(final String parentAbsPath, final int uuidBehavior) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getLockTokens() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Session impersonate(final Credentials credentials) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void importXML(final String parentAbsPath, final InputStream in, final int uuidBehavior) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isLive() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void logout() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void move(final String srcAbsPath, final String destAbsPath) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void removeLockToken(final String lt) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public AccessControlManager getAccessControlManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public RetentionManager getRetentionManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasCapability(final String methodName, final Object target, final Object[] arguments) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasPermission(final String absPath, final String actions) {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockWorkspace.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockWorkspace.java
new file mode 100644
index 0000000..6996103
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockWorkspace.java
@@ -0,0 +1,149 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import java.io.InputStream;
+
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.Session;
+import javax.jcr.Workspace;
+import javax.jcr.lock.LockManager;
+import javax.jcr.nodetype.NodeTypeManager;
+import javax.jcr.observation.ObservationManager;
+import javax.jcr.query.QueryManager;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionManager;
+
+import org.xml.sax.ContentHandler;
+
+/**
+ * Mock {@link Workspace} implementation
+ */
+class MockWorkspace implements Workspace {
+
+    private final Session session;
+    private final NamespaceRegistry namespaceRegistry = new MockNamespaceRegistry();
+    private final ObservationManager observationManager = new MockObservationManager();
+    private final NodeTypeManager nodeTypeManager = new MockNodeTypeManager();
+
+    /**
+     * @param session JCR session
+     */
+    public MockWorkspace(final Session session) {
+        this.session = session;
+    }
+
+    @Override
+    public Session getSession() {
+        return this.session;
+    }
+
+    @Override
+    public String getName() {
+        return MockJcr.DEFAULT_WORKSPACE;
+    }
+
+    @Override
+    public NamespaceRegistry getNamespaceRegistry() {
+        return this.namespaceRegistry;
+    }
+
+    @Override
+    public ObservationManager getObservationManager() {
+        return this.observationManager;
+    }
+
+    @Override
+    public NodeTypeManager getNodeTypeManager() {
+        return this.nodeTypeManager;
+    }
+
+    // --- unsupported operations ---
+    @Override
+    public void copy(final String srcAbsPath, final String destAbsPath) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void copy(final String srcWorkspace, final String srcAbsPath, final String destAbsPath) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void clone(final String srcWorkspace, final String srcAbsPath, final String destAbsPath,
+            final boolean removeExisting) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void move(final String srcAbsPath, final String destAbsPath) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void restore(final Version[] versions, final boolean removeExisting) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public LockManager getLockManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public QueryManager getQueryManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public VersionManager getVersionManager() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getAccessibleWorkspaceNames() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ContentHandler getImportContentHandler(final String parentAbsPath, final int uuidBehavior) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void importXML(final String parentAbsPath, final InputStream in, final int uuidBehavior) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void createWorkspace(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void createWorkspace(final String name, final String srcWorkspace) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void deleteWorkspace(final String name) {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/ResourceUtil.java b/src/main/java/org/apache/sling/testing/mock/jcr/ResourceUtil.java
new file mode 100644
index 0000000..f1488bb
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/ResourceUtil.java
@@ -0,0 +1,219 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+/**
+ * This is a stripped-down copy of org.apache.sling.api.resource.ResourceUtil
+ * with some methods required by the jcr-mock implementation internally.
+ */
+class ResourceUtil {
+
+    /**
+     * Resolves relative path segments '.' and '..' in the absolute path.
+     * Returns null if not possible (.. points above root) or if path is not
+     * absolute.
+     */
+    public static String normalize(String path) {
+
+        // don't care for empty paths
+        if (path.length() == 0) {
+            return path;
+        }
+
+        // prepare the path buffer with trailing slash (simplifies impl)
+        int absOffset = (path.charAt(0) == '/') ? 0 : 1;
+        char[] buf = new char[path.length() + 1 + absOffset];
+        if (absOffset == 1) {
+            buf[0] = '/';
+        }
+        path.getChars(0, path.length(), buf, absOffset);
+        buf[buf.length - 1] = '/';
+
+        int lastSlash = 0; // last slash in path
+        int numDots = 0; // number of consecutive dots after last slash
+
+        int bufPos = 0;
+        for (int bufIdx = lastSlash; bufIdx < buf.length; bufIdx++) {
+            char c = buf[bufIdx];
+            if (c == '/') {
+                if (numDots == 2) {
+                    if (bufPos == 0) {
+                        return null;
+                    }
+
+                    do {
+                        bufPos--;
+                    } while (bufPos > 0 && buf[bufPos] != '/');
+                }
+
+                lastSlash = bufIdx;
+                numDots = 0;
+            } else if (c == '.' && numDots < 2) {
+                numDots++;
+            } else {
+                // find the next slash
+                int nextSlash = bufIdx + 1;
+                while (nextSlash < buf.length && buf[nextSlash] != '/') {
+                    nextSlash++;
+                }
+
+                // append up to the next slash (or end of path)
+                if (bufPos < lastSlash) {
+                    int segLen = nextSlash - bufIdx + 1;
+                    System.arraycopy(buf, lastSlash, buf, bufPos, segLen);
+                    bufPos += segLen;
+                } else {
+                    bufPos = nextSlash;
+                }
+
+                numDots = 0;
+                lastSlash = nextSlash;
+                bufIdx = nextSlash;
+            }
+        }
+
+        String resolved;
+        if (bufPos == 0 && numDots == 0) {
+            resolved = (absOffset == 0) ? "/" : "";
+        } else if ((bufPos - absOffset) == path.length()) {
+            resolved = path;
+        } else {
+            resolved = new String(buf, absOffset, bufPos - absOffset);
+        }
+
+        return resolved;
+    }
+
+    /**
+     * Utility method returns the parent path of the given <code>path</code>,
+     * which is normalized by {@link #normalize(String)} before resolving the
+     * parent.
+     *
+     * @param path The path whose parent is to be returned.
+     * @return <code>null</code> if <code>path</code> is the root path (
+     *         <code>/</code>) or if <code>path</code> is a single name
+     *         containing no slash (<code>/</code>) characters.
+     * @throws IllegalArgumentException If the path cannot be normalized by the
+     *             {@link #normalize(String)} method.
+     * @throws NullPointerException If <code>path</code> is <code>null</code>.
+     */
+    public static String getParent(String path) {
+        if ("/".equals(path)) {
+            return null;
+        }
+
+        // normalize path (remove . and ..)
+        path = normalize(path);
+
+        // if normalized to root, there is no parent
+        if (path == null || "/".equals(path)) {
+            return null;
+        }
+
+        String workspaceName = null;
+
+        final int wsSepPos = path.indexOf(":/");
+        if (wsSepPos != -1) {
+            workspaceName = path.substring(0, wsSepPos);
+            path = path.substring(wsSepPos + 1);
+        }
+
+        // find the last slash, after which to cut off
+        int lastSlash = path.lastIndexOf('/');
+        if (lastSlash < 0) {
+            // no slash in the path
+            return null;
+        } else if (lastSlash == 0) {
+            // parent is root
+            if (workspaceName != null) {
+                return workspaceName + ":/";
+            }
+            return "/";
+        }
+
+        String parentPath = path.substring(0, lastSlash);
+        if (workspaceName != null) {
+            return workspaceName + ":" + parentPath;
+        }
+        return parentPath;
+    }
+
+    /**
+     * Utility method returns the ancestor's path at the given <code>level</code>
+     * relative to <code>path</code>, which is normalized by {@link #normalize(String)}
+     * before resolving the ancestor.
+     *
+     * <ul>
+     * <li><code>level</code> = 0 returns the <code>path</code>.</li>
+     * <li><code>level</code> = 1 returns the parent of <code>path</code>, if it exists, <code>null</code> otherwise.</li>
+     * <li><code>level</code> = 2 returns the grandparent of <code>path</code>, if it exists, <code>null</code> otherwise.</li>
+     * </ul>
+     *
+     * @param path The path whose ancestor is to be returned.
+     * @param level The relative level of the ancestor, relative to <code>path</code>.
+     * @return <code>null</code> if <code>path</code> doesn't have an ancestor at the
+     *            specified <code>level</code>.
+     * @throws IllegalArgumentException If the path cannot be normalized by the
+     *             {@link #normalize(String)} method or if <code>level</code> < 0.
+     * @throws NullPointerException If <code>path</code> is <code>null</code>.
+     * @since 2.2
+     */
+    public static String getParent(final String path, final int level) {
+        if ( level < 0 ) {
+            throw new IllegalArgumentException("level must be non-negative");
+        }
+        String result = path;
+        for(int i=0; i<level; i++) {
+            result = getParent(result);
+            if ( result == null ) {
+                break;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Utility method returns the name of the given <code>path</code>, which is
+     * normalized by {@link #normalize(String)} before resolving the name.
+     *
+     * @param path The path whose name (the last path element) is to be
+     *            returned.
+     * @return The empty string if <code>path</code> is the root path (
+     *         <code>/</code>) or if <code>path</code> is a single name
+     *         containing no slash (<code>/</code>) characters.
+     * @throws IllegalArgumentException If the path cannot be normalized by the
+     *             {@link #normalize(String)} method.
+     * @throws NullPointerException If <code>path</code> is <code>null</code>.
+     */
+    public static String getName(String path) {
+        if ("/".equals(path)) {
+            return "";
+        }
+
+        // normalize path (remove . and ..)
+        path = normalize(path);
+        if ("/".equals(path)) {
+            return "";
+        }
+
+        // find the last slash
+        return path.substring(path.lastIndexOf('/') + 1);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/package-info.java b/src/main/java/org/apache/sling/testing/mock/jcr/package-info.java
new file mode 100644
index 0000000..cf6e2e7
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+/**
+ * Mock implementation of selected JCR APIs.
+ */
+package org.apache.sling.testing.mock.jcr;
+
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
new file mode 100644
index 0000000..544541e
--- /dev/null
+++ b/src/site/markdown/index.md
@@ -0,0 +1,38 @@
+## About JCR Mocks
+
+Mock implementation of selected JCR APIs.
+
+### Maven Dependency
+
+```xml
+<dependency>
+  <groupId>org.apache.sling</groupId>
+  <artifactId>org.apache.sling.testing.jcr-mock</artifactId>
+  <version>1.0.0-SNAPHOT</version>
+</dependency>
+```
+
+### Documentation
+
+* [Usage](usage.html)
+* [API Documentation](apidocs/)
+* [Changelog](changes-report.html)
+
+### Implemented mock features
+
+The mock implementation supports:
+
+* Reading and writing all data (primitive values, arrays, binary data) via the JCR API
+* Creating any number of nodes and properties (stored in-memory in a hash map)
+* Register namespaces
+
+The following features are *not supported*:
+
+* Node types are supported in the API, but their definitions and constraints are not applied
+* Versioning not supported
+* Search not supported
+* Transactions not supported
+* Observation events can be registered but are ignored
+* Access control always grants access
+* Exporting/Importing data via document and system views not supported 
+* Workspace management methods not supported
diff --git a/src/site/markdown/usage.md b/src/site/markdown/usage.md
new file mode 100644
index 0000000..9ae3ce7
--- /dev/null
+++ b/src/site/markdown/usage.md
@@ -0,0 +1,17 @@
+## Usage
+
+### Getting JCR mock objects
+
+The factory class `MockJcr` allows to instantiate the different mock implementations.
+
+Example:
+
+```java
+// get session
+Session session = MockJcr.newSession();
+
+// get repository
+Repository repository = MockJcr.newRepository();
+```
+
+The repository is empty and contains only the root node. You can use the JCR API to fill it with content.
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/AbstractItemTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/AbstractItemTest.java
new file mode 100644
index 0000000..e7bd268
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/AbstractItemTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public abstract class AbstractItemTest {
+
+    private Session session;
+    private Node rootNode;
+    private Node node1;
+    private Property prop1;
+    private Node node11;
+
+    @Before
+    public void setUp() throws RepositoryException {
+        this.session = MockJcr.newSession();
+        this.rootNode = this.session.getRootNode();
+        this.node1 = this.rootNode.addNode("node1");
+        this.prop1 = this.node1.setProperty("prop1", "value1");
+        this.node11 = this.node1.addNode("node11");
+    }
+
+    @Test
+    public void testGetName() throws RepositoryException {
+        assertEquals("node1", this.node1.getName());
+        assertEquals("prop1", this.prop1.getName());
+    }
+
+    @Test
+    public void testGetParent() throws RepositoryException {
+        assertSame(this.rootNode, this.node1.getParent());
+        assertSame(this.node1, this.prop1.getParent());
+        assertSame(this.node1, this.node11.getParent());
+    }
+
+    @Test
+    public void testGetAncestor() throws RepositoryException {
+        assertSame(this.node11, this.node11.getAncestor(0));
+        assertSame(this.node1, this.node11.getAncestor(1));
+        assertSame(this.rootNode, this.node11.getAncestor(2));
+    }
+
+    @Test(expected = ItemNotFoundException.class)
+    public void testGetAncestorNegative() throws RepositoryException {
+        assertSame(this.node11, this.node11.getAncestor(-1));
+    }
+
+    @Test(expected = ItemNotFoundException.class)
+    public void testGetAncestorTooDeep() throws RepositoryException {
+        this.node11.getAncestor(3);
+    }
+
+    @Test
+    public void testGetDepth() throws RepositoryException {
+        assertEquals(2, this.node11.getDepth());
+        assertEquals(1, this.node1.getDepth());
+        assertEquals(0, this.rootNode.getDepth());
+    }
+
+    @Test
+    public void testModifiedNew() {
+        // methods return always false
+        assertFalse(this.node1.isModified());
+        assertFalse(this.node1.isNew());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockNodeTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockNodeTest.java
new file mode 100644
index 0000000..f02df0e
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockNodeTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockNodeTest {
+
+    private Session session;
+    private Node rootNode;
+    private Node node1;
+    private Property prop1;
+    private Node node11;
+
+    @Before
+    public void setUp() throws RepositoryException {
+        this.session = MockJcr.newSession();
+        this.rootNode = this.session.getRootNode();
+        this.node1 = this.rootNode.addNode("node1");
+        this.prop1 = this.node1.setProperty("prop1", "value1");
+        this.node11 = this.node1.addNode("node11");
+    }
+
+    @Test
+    public void testGetNodes() throws RepositoryException {
+        NodeIterator nodes = this.node1.getNodes();
+        assertEquals(1, nodes.getSize());
+        assertSame(this.node11, nodes.next());
+
+        assertTrue(this.node1.hasNodes());
+        assertFalse(this.node11.hasNodes());
+
+        nodes = this.node1.getNodes("^node.*$");
+        assertEquals(1, nodes.getSize());
+        assertSame(this.node11, nodes.next());
+
+        nodes = this.node1.getNodes("unknown?");
+        assertEquals(0, nodes.getSize());
+    }
+
+    @Test
+    public void testGetProperties() throws RepositoryException {
+        PropertyIterator properties = this.node1.getProperties();
+        assertEquals(1, properties.getSize());
+        assertSame(this.prop1, properties.next());
+
+        assertTrue(this.node1.hasProperties());
+        assertFalse(this.node11.hasProperties());
+
+        properties = this.node1.getProperties("^prop.*$");
+        assertEquals(1, properties.getSize());
+        assertSame(this.prop1, properties.next());
+
+        properties = this.node1.getProperties("unknown?");
+        assertEquals(0, properties.getSize());
+    }
+
+    @Test
+    public void testIsNode() {
+        assertTrue(this.node1.isNode());
+        assertFalse(this.prop1.isNode());
+    }
+
+    @Test
+    public void testHasNode() throws RepositoryException {
+        assertTrue(this.node1.hasNode("node11"));
+        assertFalse(this.node1.hasNode("node25"));
+    }
+
+    @Test
+    public void testHasProperty() throws RepositoryException {
+        assertTrue(this.node1.hasProperty("prop1"));
+        assertFalse(this.node1.hasProperty("prop25"));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testGetUUID() throws RepositoryException {
+        assertEquals(this.node1.getIdentifier(), this.node1.getUUID());
+    }
+
+    @Test
+    public void testGetPrimaryItem() throws RepositoryException {
+        Node dataParent = this.node1.addNode("dataParent");
+        Property dataProperty = dataParent.setProperty(JcrConstants.JCR_DATA, "data");
+        assertSame(dataProperty, dataParent.getPrimaryItem());
+
+        Node contentParent = this.node1.addNode("contentParent");
+        Node contentNode = contentParent.addNode(JcrConstants.JCR_CONTENT);
+        assertSame(contentNode, contentParent.getPrimaryItem());
+    }
+
+    @Test(expected = ItemNotFoundException.class)
+    public void testGetPrimaryItemNotFound() throws RepositoryException {
+        this.node1.getPrimaryItem();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java
new file mode 100644
index 0000000..a8f30a4
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockPropertyTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.value.BinaryValue;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockPropertyTest {
+
+    private Session session;
+    private Node rootNode;
+    private Node node1;
+
+    @Before
+    public void setUp() throws RepositoryException {
+        this.session = MockJcr.newSession();
+        this.rootNode = this.session.getRootNode();
+        this.node1 = this.rootNode.addNode("node1");
+    }
+
+    @Test
+    public void testRemove() throws RepositoryException {
+        this.node1.setProperty("prop1", "value1");
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals("value1", prop1.getString());
+
+        prop1.remove();
+        assertFalse(this.node1.hasProperty("prop1"));
+    }
+
+    @Test
+    public void testString() throws RepositoryException {
+        this.node1.setProperty("prop1", "value1");
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals("value1", prop1.getString());
+        assertEquals("value1", prop1.getValue().getString());
+
+        prop1.setValue("value2");
+        assertEquals("value2", prop1.getString());
+        assertEquals("value2", prop1.getValue().getString());
+
+        assertFalse(prop1.isMultiple());
+        assertFalse(prop1.getDefinition().isMultiple());
+        assertEquals(6, prop1.getLength());
+    }
+
+    @Test
+    public void testStringArray() throws RepositoryException {
+        String[] value1 = new String[] { "aaa", "bbb" };
+        this.node1.setProperty("prop1", value1);
+        Property prop1 = this.node1.getProperty("prop1");
+
+        Value[] values = prop1.getValues();
+        for (int i = 0; i < values.length; i++) {
+            assertEquals("value #" + i, value1[i], values[i].getString());
+        }
+
+        String[] value2 = new String[] { "cc" };
+        prop1.setValue(value2);
+        values = prop1.getValues();
+        for (int i = 0; i < values.length; i++) {
+            assertEquals("value #" + i, value2[i], values[i].getString());
+        }
+
+        assertTrue(prop1.isMultiple());
+        assertTrue(prop1.getDefinition().isMultiple());
+        assertArrayEquals(new long[] { 2 }, prop1.getLengths());
+    }
+
+    @Test
+    public void testBoolean() throws RepositoryException {
+        this.node1.setProperty("prop1", true);
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals(true, prop1.getBoolean());
+        assertEquals(true, prop1.getValue().getBoolean());
+
+        prop1.setValue(false);
+        assertEquals(false, prop1.getBoolean());
+        assertEquals(false, prop1.getValue().getBoolean());
+    }
+
+    @Test
+    public void testDouble() throws RepositoryException {
+        this.node1.setProperty("prop1", 1.5d);
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals(1.5d, prop1.getDouble(), 0.001d);
+        assertEquals(1.5d, prop1.getValue().getDouble(), 0.001d);
+
+        prop1.setValue(Double.MAX_VALUE);
+        assertEquals(Double.MAX_VALUE, prop1.getDouble(), 0.001d);
+        assertEquals(Double.MAX_VALUE, prop1.getValue().getDouble(), 0.001d);
+    }
+
+    @Test
+    public void testLong() throws RepositoryException {
+        this.node1.setProperty("prop1", 5L);
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals(5L, prop1.getLong());
+        assertEquals(5L, prop1.getValue().getLong());
+
+        prop1.setValue(Long.MAX_VALUE);
+        assertEquals(Long.MAX_VALUE, prop1.getLong());
+        assertEquals(Long.MAX_VALUE, prop1.getValue().getLong());
+    }
+
+    @Test
+    public void testBigDecimal() throws RepositoryException {
+        this.node1.setProperty("prop1", new BigDecimal("1.5"));
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals(new BigDecimal("1.5"), prop1.getDecimal());
+        assertEquals(new BigDecimal("1.5"), prop1.getValue().getDecimal());
+
+        prop1.setValue(new BigDecimal("99999999.99999"));
+        assertEquals(new BigDecimal("99999999.99999"), prop1.getDecimal());
+        assertEquals(new BigDecimal("99999999.99999"), prop1.getValue().getDecimal());
+    }
+
+    @Test
+    public void testCalendar() throws RepositoryException {
+        Calendar value1 = Calendar.getInstance();
+
+        this.node1.setProperty("prop1", value1);
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals(value1, prop1.getDate());
+        assertEquals(value1, prop1.getValue().getDate());
+
+        Calendar value2 = Calendar.getInstance();
+        value2.add(Calendar.MONTH, -1);
+
+        prop1.setValue(value2);
+        assertEquals(value2, prop1.getDate());
+        assertEquals(value2, prop1.getValue().getDate());
+    }
+
+    @Test
+    public void testBinary() throws RepositoryException, IOException {
+        byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
+
+        this.node1.setProperty("prop1", new BinaryValue(value1).getBinary());
+        Property prop1 = this.node1.getProperty("prop1");
+        assertArrayEquals(value1, IOUtils.toByteArray(prop1.getBinary().getStream()));
+        assertArrayEquals(value1, IOUtils.toByteArray(prop1.getValue().getBinary().getStream()));
+
+        byte[] value2 = new byte[] { 0x04, 0x05, 0x06 };
+
+        prop1.setValue(new BinaryValue(value2).getBinary());
+        assertArrayEquals(value2, IOUtils.toByteArray(prop1.getBinary().getStream()));
+        assertArrayEquals(value2, IOUtils.toByteArray(prop1.getValue().getBinary().getStream()));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    public void testInputStream() throws RepositoryException, IOException {
+        byte[] value1 = new byte[] { 0x01, 0x01, 0x03 };
+
+        this.node1.setProperty("prop1", new ByteArrayInputStream(value1));
+        Property prop1 = this.node1.getProperty("prop1");
+        assertArrayEquals(value1, IOUtils.toByteArray(prop1.getStream()));
+
+        byte[] value2 = new byte[] { 0x04, 0x05, 0x06 };
+
+        prop1.setValue(new ByteArrayInputStream(value2));
+        assertArrayEquals(value2, IOUtils.toByteArray(prop1.getValue().getStream()));
+    }
+
+    @Test
+    public void testValue() throws RepositoryException {
+        this.node1.setProperty("prop1", this.session.getValueFactory().createValue("value1"));
+        Property prop1 = this.node1.getProperty("prop1");
+        assertEquals("value1", prop1.getString());
+        assertEquals("value1", prop1.getValue().getString());
+
+        prop1.setValue(this.session.getValueFactory().createValue("value2"));
+        assertEquals("value2", prop1.getString());
+        assertEquals("value2", prop1.getValue().getString());
+
+        assertFalse(prop1.isMultiple());
+        assertFalse(prop1.getDefinition().isMultiple());
+        assertEquals(6, prop1.getLength());
+    }
+
+    @Test
+    public void testValueArray() throws RepositoryException {
+        Value[] value1 = new Value[] { this.session.getValueFactory().createValue("aaa"),
+                this.session.getValueFactory().createValue("bbb") };
+        this.node1.setProperty("prop1", value1);
+        Property prop1 = this.node1.getProperty("prop1");
+
+        Value[] values = prop1.getValues();
+        for (int i = 0; i < values.length; i++) {
+            assertEquals("value #" + i, value1[i].getString(), values[i].getString());
+        }
+
+        Value[] value2 = new Value[] { this.session.getValueFactory().createValue("cc") };
+        prop1.setValue(value2);
+        values = prop1.getValues();
+        for (int i = 0; i < values.length; i++) {
+            assertEquals("value #" + i, value2[i].getString(), values[i].getString());
+        }
+
+        assertTrue(prop1.isMultiple());
+        assertTrue(prop1.getDefinition().isMultiple());
+        assertArrayEquals(new long[] { 2 }, prop1.getLengths());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockRepositoryTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockRepositoryTest.java
new file mode 100644
index 0000000..6143456
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockRepositoryTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.SimpleCredentials;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockRepositoryTest {
+
+    private static final String USER_NAME = "user";
+    private static final char[] PASSWORD = "pwd".toCharArray();
+
+    private Repository repository;
+
+    @Before
+    public void setUp() {
+        this.repository = MockJcr.newRepository();
+    }
+
+    @Test
+    public void testLogin() throws RepositoryException {
+        assertNotNull(this.repository.login());
+        assertNotNull(this.repository.login(new SimpleCredentials(USER_NAME, PASSWORD)));
+        assertNotNull(this.repository.login(MockJcr.DEFAULT_WORKSPACE));
+        assertNotNull(this.repository.login(new SimpleCredentials(USER_NAME, PASSWORD), MockJcr.DEFAULT_WORKSPACE));
+    }
+
+    @Test
+    public void testDescriptor() {
+        assertEquals(0, this.repository.getDescriptorKeys().length);
+        assertNull(this.repository.getDescriptor("test"));
+        assertNull(this.repository.getDescriptorValue("test"));
+        assertNull(this.repository.getDescriptorValues("test"));
+        assertFalse(this.repository.isStandardDescriptor("test"));
+        assertFalse(this.repository.isSingleValueDescriptor("test"));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java
new file mode 100644
index 0000000..4c297c1
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java
@@ -0,0 +1,228 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+
+public class MockSessionTest {
+
+    private Session session;
+
+    @Before
+    public void setUp() {
+        this.session = MockJcr.newSession();
+    }
+
+    @Test
+    public void testEmptySession() throws RepositoryException {
+        Node rootNode = this.session.getRootNode();
+        assertNotNull(rootNode);
+        assertFalse(rootNode.getProperties().hasNext());
+        assertFalse(rootNode.getNodes().hasNext());
+    }
+
+    @Test
+    public void testNodePropertyCreateRead() throws RepositoryException {
+        Node rootNode = this.session.getNode("/");
+        assertSame(rootNode, this.session.getRootNode());
+
+        Node node1 = rootNode.addNode("node1");
+        node1.setProperty("prop1a", "value1a");
+        node1.setProperty("prop1b", "value1b");
+
+        Node node2 = rootNode.addNode("node2");
+        node2.setProperty("prop2", "value2");
+
+        assertSame(node1, rootNode.getNode("node1"));
+        assertSame(node1, this.session.getNode("/node1"));
+        assertSame(node1, this.session.getItem("/node1"));
+        assertSame(node1, this.session.getNodeByIdentifier(node1.getIdentifier()));
+        assertTrue(this.session.nodeExists("/node1"));
+        assertTrue(this.session.itemExists("/node1"));
+        assertSame(node2, rootNode.getNode("node2"));
+        assertSame(node2, this.session.getNode("/node2"));
+        assertSame(node2, this.session.getItem("/node2"));
+        assertSame(node2, this.session.getNodeByIdentifier(node2.getIdentifier()));
+        assertTrue(this.session.nodeExists("/node2"));
+        assertTrue(this.session.itemExists("/node2"));
+
+        Property prop1a = node1.getProperty("prop1a");
+        Property prop1b = node1.getProperty("prop1b");
+        Property prop2 = node2.getProperty("prop2");
+
+        assertSame(prop1a, this.session.getProperty("/node1/prop1a"));
+        assertSame(prop1a, this.session.getItem("/node1/prop1a"));
+        assertTrue(this.session.propertyExists("/node1/prop1a"));
+        assertTrue(this.session.itemExists("/node1/prop1a"));
+        assertSame(prop1b, this.session.getProperty("/node1/prop1b"));
+        assertSame(prop1b, this.session.getItem("/node1/prop1b"));
+        assertTrue(this.session.propertyExists("/node1/prop1b"));
+        assertTrue(this.session.itemExists("/node1/prop1b"));
+        assertSame(prop2, this.session.getProperty("/node2/prop2"));
+        assertSame(prop2, this.session.getItem("/node2/prop2"));
+        assertTrue(this.session.propertyExists("/node2/prop2"));
+        assertTrue(this.session.itemExists("/node2/prop2"));
+
+        assertEquals("value1a", prop1a.getString());
+        assertEquals("value1b", prop1b.getString());
+        assertEquals("value2", prop2.getString());
+
+        assertFalse(this.session.propertyExists("/node1"));
+        assertFalse(this.session.nodeExists("/node1/prop1a"));
+
+        assertEquals(JcrConstants.NT_UNSTRUCTURED, node1.getPrimaryNodeType().getName());
+        assertTrue(node1.isNodeType(JcrConstants.NT_UNSTRUCTURED));
+        assertTrue(node1.getPrimaryNodeType().isNodeType(JcrConstants.NT_UNSTRUCTURED));
+
+    }
+
+    @Test
+    public void testNodeRemove() throws RepositoryException {
+        Node rootNode = this.session.getRootNode();
+        Node node1 = rootNode.addNode("node1");
+        assertTrue(this.session.itemExists("/node1"));
+        node1.remove();
+        assertFalse(this.session.itemExists("/node1"));
+        assertFalse(rootNode.getNodes().hasNext());
+    }
+
+    @Test
+    public void testNodesWithSpecialNames() throws RepositoryException {
+        Node rootNode = this.session.getRootNode();
+
+        Node node1 = rootNode.addNode("node1.ext");
+        Node node11 = node1.addNode("Node Name With Spaces");
+        node11.setProperty("prop11", "value11");
+        Node node12 = node1.addNode("node12_ext");
+        node12.setProperty("prop12", "value12");
+
+        assertTrue(this.session.itemExists("/node1.ext"));
+        assertTrue(this.session.itemExists("/node1.ext/Node Name With Spaces"));
+        assertTrue(this.session.itemExists("/node1.ext/node12_ext"));
+
+        assertEquals("value11", node11.getProperty("prop11").getString());
+        assertEquals("value12", node12.getProperty("prop12").getString());
+
+        NodeIterator nodes = node1.getNodes();
+        assertEquals(2, nodes.getSize());
+    }
+
+    @Test
+    public void testItemsExists() throws RepositoryException {
+        assertFalse(this.session.nodeExists("/node1"));
+        assertFalse(this.session.itemExists("/node2"));
+        assertFalse(this.session.propertyExists("/node1/prop1"));
+    }
+
+    @Test(expected = PathNotFoundException.class)
+    public void testNodeNotFoundException() throws RepositoryException {
+        this.session.getNode("/node1");
+    }
+
+    @Test(expected = PathNotFoundException.class)
+    public void testPropertyNotFoundException() throws RepositoryException {
+        this.session.getProperty("/node1/prop1");
+    }
+
+    @Test(expected = PathNotFoundException.class)
+    public void testItemNotFoundException() throws RepositoryException {
+        this.session.getItem("/node2");
+    }
+
+    @Test(expected = ItemNotFoundException.class)
+    public void testIdentifierFoundException() throws RepositoryException {
+        this.session.getNodeByIdentifier("unknown");
+    }
+
+    @Test
+    public void testNamespaces() throws RepositoryException {
+        // test initial namespaces
+        assertArrayEquals(new String[] { "jcr" }, this.session.getNamespacePrefixes());
+        assertEquals("http://www.jcp.org/jcr/1.0", this.session.getNamespaceURI("jcr"));
+        assertEquals("jcr", this.session.getNamespacePrefix("http://www.jcp.org/jcr/1.0"));
+
+        // add dummy namespace
+        this.session.setNamespacePrefix("dummy", "http://mydummy");
+
+        assertEquals(ImmutableSet.of("jcr", "dummy"), ImmutableSet.copyOf(this.session.getNamespacePrefixes()));
+        assertEquals("http://mydummy", this.session.getNamespaceURI("dummy"));
+        assertEquals("dummy", this.session.getNamespacePrefix("http://mydummy"));
+
+        // test via namespace registry
+        NamespaceRegistry namespaceRegistry = this.session.getWorkspace().getNamespaceRegistry();
+
+        assertEquals(ImmutableSet.of("jcr", "dummy"), ImmutableSet.copyOf(namespaceRegistry.getPrefixes()));
+        assertEquals(ImmutableSet.of("http://www.jcp.org/jcr/1.0", "http://mydummy"),
+                ImmutableSet.copyOf(namespaceRegistry.getURIs()));
+        assertEquals("http://mydummy", namespaceRegistry.getURI("dummy"));
+        assertEquals("dummy", namespaceRegistry.getPrefix("http://mydummy"));
+
+        // remove dummy namespace
+        namespaceRegistry.unregisterNamespace("dummy");
+
+        assertEquals(ImmutableSet.of("jcr"), ImmutableSet.copyOf(this.session.getNamespacePrefixes()));
+        assertEquals("http://www.jcp.org/jcr/1.0", this.session.getNamespaceURI("jcr"));
+        assertEquals("jcr", this.session.getNamespacePrefix("http://www.jcp.org/jcr/1.0"));
+    }
+
+    @Test
+    public void testUserId() {
+        assertEquals("mockedUserId", this.session.getUserID());
+    }
+
+    @Test
+    public void testSaveRefresh() throws RepositoryException {
+        // methods can be called without any effect
+        assertFalse(this.session.hasPendingChanges());
+        this.session.save();
+        this.session.refresh(true);
+        this.session.refresh(false);
+    }
+
+    @Test
+    public void testGetRepository() {
+        assertNotNull(this.session.getRepository());
+    }
+
+    @Test
+    public void testCheckPermission() throws RepositoryException {
+        this.session.checkPermission("/any/path", "anyActions");
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockWorkspaceTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockWorkspaceTest.java
new file mode 100644
index 0000000..662a623
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockWorkspaceTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Workspace;
+import javax.jcr.observation.ObservationManager;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockWorkspaceTest {
+
+    private Workspace underTest;
+
+    @Before
+    public void setUp() {
+        underTest = MockJcr.newSession().getWorkspace();
+    }
+
+    @Test
+    public void testName() {
+        assertEquals(MockJcr.DEFAULT_WORKSPACE, underTest.getName());
+    }
+
+    @Test
+    public void testNameSpaceRegistry() throws RepositoryException {
+        assertNotNull(underTest.getNamespaceRegistry());
+    }
+
+    @Test
+    public void testObservationManager() throws RepositoryException {
+        // just mage sure listener methods can be called, although they do
+        // nothing
+        ObservationManager observationManager = underTest.getObservationManager();
+        observationManager.addEventListener(null, 0, null, false, null, null, false);
+        observationManager.removeEventListener(null);
+    }
+
+    @Test
+    public void testNodeTypeManager() throws RepositoryException {
+        assertNotNull(underTest.getNodeTypeManager());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/ResourceUtilTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/ResourceUtilTest.java
new file mode 100644
index 0000000..05256bc
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/ResourceUtilTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.sling.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+public class ResourceUtilTest {
+
+    @Test
+    public void testResolveRelativeSegments() {
+
+        assertEquals("/", ResourceUtil.normalize("/"));
+        assertEquals("/", ResourceUtil.normalize("///"));
+
+        assertEquals("/a/b/c", ResourceUtil.normalize("/a//b/c"));
+        assertEquals("/a/b/c", ResourceUtil.normalize("/a/b//c"));
+        assertEquals("/a/b/c", ResourceUtil.normalize("/a///b///c"));
+        assertEquals("/a/b/c", ResourceUtil.normalize("/a/b/c/"));
+        assertEquals("/a/b/c", ResourceUtil.normalize("/a/b/c//"));
+        assertEquals("/a/b/c", ResourceUtil.normalize("/a/b/c///"));
+
+        assertEquals("/az/bz/cz", ResourceUtil.normalize("/az//bz/cz"));
+        assertEquals("/az/bz/cz", ResourceUtil.normalize("/az/bz//cz"));
+        assertEquals("/az/bz/cz", ResourceUtil.normalize("/az///bz///cz"));
+        assertEquals("/az/bz/cz", ResourceUtil.normalize("/az/bz/cz/"));
+        assertEquals("/az/bz/cz", ResourceUtil.normalize("/az/bz/cz//"));
+        assertEquals("/az/bz/cz", ResourceUtil.normalize("/az/bz/cz///"));
+
+        assertEquals("/a", ResourceUtil.normalize("/a"));
+        assertEquals("/a", ResourceUtil.normalize("//a"));
+        assertEquals("/a", ResourceUtil.normalize("///a"));
+
+        assertEquals("/az", ResourceUtil.normalize("/az"));
+        assertEquals("/az", ResourceUtil.normalize("//az"));
+        assertEquals("/az", ResourceUtil.normalize("///az"));
+
+        assertEquals("/", ResourceUtil.normalize("/."));
+        assertEquals("/a", ResourceUtil.normalize("/a/."));
+        assertEquals("/a", ResourceUtil.normalize("/./a"));
+        assertEquals("/a/b", ResourceUtil.normalize("/a/./b"));
+        assertEquals("/a/b", ResourceUtil.normalize("/a/b/."));
+        assertEquals("/a/b", ResourceUtil.normalize("/a/./b/."));
+
+        assertEquals("/", ResourceUtil.normalize("/."));
+        assertEquals("/az", ResourceUtil.normalize("/az/."));
+        assertEquals("/az", ResourceUtil.normalize("/./az"));
+        assertEquals("/az/bz", ResourceUtil.normalize("/az/./bz"));
+        assertEquals("/az/bz", ResourceUtil.normalize("/az/bz/."));
+        assertEquals("/az/bz", ResourceUtil.normalize("/az/./bz/."));
+
+        assertNull(ResourceUtil.normalize("/.."));
+        assertNull(ResourceUtil.normalize("/.."));
+        assertEquals("/", ResourceUtil.normalize("/a/.."));
+        assertEquals("/a", ResourceUtil.normalize("/a/b/.."));
+        assertEquals("/", ResourceUtil.normalize("/a/b/../.."));
+        assertNull(ResourceUtil.normalize("/a/b/../../.."));
+
+        assertNull(ResourceUtil.normalize("/.."));
+        assertNull(ResourceUtil.normalize("/.."));
+        assertEquals("/", ResourceUtil.normalize("/az/.."));
+        assertEquals("/az", ResourceUtil.normalize("/az/bz/.."));
+        assertEquals("/", ResourceUtil.normalize("/az/bz/../.."));
+        assertNull(ResourceUtil.normalize("/az/bz/../../.."));
+
+        assertEquals("/b", ResourceUtil.normalize("/a/../b"));
+        assertEquals("/a/c", ResourceUtil.normalize("/a/b/../c"));
+        assertEquals("/c", ResourceUtil.normalize("/a/b/../../c"));
+        assertNull(ResourceUtil.normalize("/a/b/../../../c"));
+
+        assertEquals("/bz", ResourceUtil.normalize("/az/../bz"));
+        assertEquals("/az/cz", ResourceUtil.normalize("/az/bz/../cz"));
+        assertEquals("/cz", ResourceUtil.normalize("/az/bz/../../cz"));
+        assertNull(ResourceUtil.normalize("/az/bz/../../../cz"));
+
+        assertEquals("/...", ResourceUtil.normalize("/..."));
+        assertEquals("/a/...", ResourceUtil.normalize("/a/..."));
+        assertEquals("/a/b/...", ResourceUtil.normalize("/a/b/..."));
+
+        assertEquals("/az/...", ResourceUtil.normalize("/az/..."));
+        assertEquals("/az/bz/...", ResourceUtil.normalize("/az/bz/..."));
+
+        try {
+            ResourceUtil.normalize(null);
+            fail("Resolving null expects NullPointerException");
+        } catch (NullPointerException npe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testResolveRelativeSegmentsRelative() {
+        assertEquals("a/b", ResourceUtil.normalize("a/b"));
+        assertEquals("a", ResourceUtil.normalize("a/b/.."));
+
+        assertEquals("b", ResourceUtil.normalize("a/../b"));
+        assertEquals("a/c", ResourceUtil.normalize("a/b/../c"));
+        assertEquals("c", ResourceUtil.normalize("a/b/../../c"));
+        assertEquals("", ResourceUtil.normalize("a/b/../.."));
+        assertEquals("a/c/d", ResourceUtil.normalize("a/b/../c/d"));
+        assertNull(ResourceUtil.normalize("a/b/../../../c"));
+
+        assertEquals("a/b/c", ResourceUtil.normalize("a/b/c"));
+        assertEquals("az/bz/cz", ResourceUtil.normalize("az/bz/cz"));
+        assertEquals("", ResourceUtil.normalize(""));
+    }
+
+    @Test
+    public void testGetParent() {
+        assertNull(ResourceUtil.getParent("/"));
+        assertNull(ResourceUtil.getParent("/.."));
+
+        assertEquals("/", ResourceUtil.getParent("/b"));
+        assertEquals("b/c", ResourceUtil.getParent("b/c/d"));
+        assertEquals("/b/c", ResourceUtil.getParent("/b/c/d"));
+
+        assertNull(ResourceUtil.getParent("b"));
+        assertNull(ResourceUtil.getParent("/b/.."));
+
+        assertEquals("security:/", ResourceUtil.getParent("security:/b"));
+        assertEquals("security:/b", ResourceUtil.getParent("security:/b/c"));
+        assertEquals("security:/b/c", ResourceUtil.getParent("security:/b/c/d"));
+    }
+
+    @Test
+    public void testGetName() {
+        assertEquals("", ResourceUtil.getName("/"));
+        assertEquals("", ResourceUtil.getName("/a/.."));
+
+        assertEquals("c", ResourceUtil.getName("c"));
+        assertEquals("c", ResourceUtil.getName("/c"));
+
+        assertEquals("c", ResourceUtil.getName("b/c"));
+        assertEquals("c", ResourceUtil.getName("/b/c"));
+
+        assertEquals("c", ResourceUtil.getName("b/c/"));
+        assertEquals("c", ResourceUtil.getName("/b/c/"));
+
+        assertEquals("b", ResourceUtil.getName("b/c/.."));
+        assertEquals("b", ResourceUtil.getName("/b/c/.."));
+        assertEquals("", ResourceUtil.getName("/b/c/../.."));
+    }
+
+    @Test
+    public void testGetParentLevel() throws Exception {
+        boolean caughtNullPointerException = false;
+        try {
+            ResourceUtil.getParent(null, 4);
+        } catch (NullPointerException e) {
+            // Expected exception
+            caughtNullPointerException = true;
+        } catch (Exception e) {
+            fail("Expected NullPointerException, but caught " + e.getClass().getName() + " instead.");
+        }
+        if (!caughtNullPointerException) {
+            fail("Expected NullPointerException, but no exception was thrown.");
+        }
+
+        boolean caughtIllegalArgumentException = false;
+        try {
+            ResourceUtil.getParent("/a/b", -2);
+        } catch (IllegalArgumentException e) {
+            // Expected exception
+            caughtIllegalArgumentException = true;
+        } catch (Exception e) {
+            fail("Expected IllegalArgumentException, but caught " + e.getClass().getName() + " instead.");
+        }
+        if (!caughtIllegalArgumentException) {
+            fail("Expected IllegalArgumentException, but no exception was thrown.");
+        }
+
+        assertNull(ResourceUtil.getParent("/a", 4));
+        assertNull(ResourceUtil.getParent("/", 1));
+        assertNull(ResourceUtil.getParent("b/c", 2));
+        assertNull(ResourceUtil.getParent("/b/..", 1));
+        assertNull(ResourceUtil.getParent("b", 1));
+        assertNull(ResourceUtil.getParent("", 3));
+        assertNull(ResourceUtil.getParent("/..", 1));
+        assertNull(ResourceUtil.getParent("security:/b", 2));
+        assertNull(ResourceUtil.getParent("/b///", 2));
+
+        assertEquals("", ResourceUtil.getParent("", 0));
+        assertEquals("b", ResourceUtil.getParent("b", 0));
+        assertEquals("/", ResourceUtil.getParent("/", 0));
+        assertEquals("/a/b", ResourceUtil.getParent("/a/b", 0));
+        assertEquals("security:/b", ResourceUtil.getParent("security:/b", 0));
+
+        assertEquals("/", ResourceUtil.getParent("/b", 1));
+        assertEquals("b", ResourceUtil.getParent("b/c", 1));
+        assertEquals("b/c", ResourceUtil.getParent("b/c/d", 1));
+        assertEquals("/b/c", ResourceUtil.getParent("/b/c/d", 1));
+        assertEquals("security:/", ResourceUtil.getParent("security:/b", 1));
+        assertEquals("security:/b", ResourceUtil.getParent("security:/b/c", 1));
+        assertEquals("security:/b/c", ResourceUtil.getParent("security:/b/c/d", 1));
+
+        assertEquals("b", ResourceUtil.getParent("b/c/d", 2));
+        assertEquals("b/c", ResourceUtil.getParent("b/c/d/e", 2));
+        assertEquals("/", ResourceUtil.getParent("/b/c/d", 3));
+        assertEquals("/", ResourceUtil.getParent("/b///", 1));
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.