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:16:30 UTC

[sling-org-apache-sling-superimposing] 01/07: SLING-1778 initial sling superimposing resource provider implementation

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

rombert pushed a commit to annotated tag org.apache.sling.superimposing-0.2.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-superimposing.git

commit fec837e5f36c8a3ab4d04547b9873cda90ec2f2b
Author: sseifert <ss...@unknown>
AuthorDate: Mon Sep 29 16:20:05 2014 +0000

    SLING-1778 initial sling superimposing resource provider implementation
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/superimposing@1628214 13f79535-47bb-0310-9956-ffa450edef68
---
 README.md                                          |  61 +++
 pom.xml                                            | 162 ++++++++
 .../sling/superimposing/SuperimposingManager.java  |  41 ++
 .../SuperimposingResourceProvider.java             |  66 ++++
 .../impl/SuperimposingManagerImpl.java             | 428 +++++++++++++++++++++
 .../superimposing/impl/SuperimposingResource.java  |  92 +++++
 .../impl/SuperimposingResourceIterator.java        |  69 ++++
 .../impl/SuperimposingResourceProviderImpl.java    | 259 +++++++++++++
 .../SLING-INF/nodetypes/superimposing.cnd          |  45 +++
 .../impl/SuperimposingManagerImplTest.java         | 375 ++++++++++++++++++
 .../impl/SuperimposingResourceIteratorTest.java    |  91 +++++
 .../SuperimposingResourceProviderImplTest.java     | 241 ++++++++++++
 .../impl/SuperimposingResourceTest.java            |  92 +++++
 13 files changed, 2022 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5533198
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+Sling Superimposing Resource Provider
+===================
+
+### About
+
+The Superimposing Resource Provider is an extension for the [Apache Sling](http://sling.apache.org/) framework. It implements the [ResourceProvider](http://sling.apache.org/apidocs/sling6/org/apache/sling/api/resource/ResourceProvider.html) interface.
+
+Goals of the solution:
+
+* Mirroring resource trees
+ * Reflect changes from master tree
+ * Avoid unnecessary copies
+* Superimposing resources
+ * Add
+ * Remove
+ * Overlay
+
+There is a presentation from [adaptTo() 2013](http://adaptto.org) with more background information:<br/>
+[Superimposing Content Presentation adaptTo() 2013](http://www.pro-vision.de/content/medialib/pro-vision/production/adaptto/2013/adaptto2013-lightning-superimposing-content-julian-sedding-stefa/_jcr_content/renditions/rendition.file/adaptto2013-lightning-superimposing-content-julian-sedding-stefan-seifert.pdf)
+
+The implementation of this provider is based on the great work of Julian Sedding from [SLING-1778](https://issues.apache.org/jira/browse/SLING-1778).
+
+
+### How to use
+
+Preparations:
+
+* Deploy the Superimposing Resource Provider Bundle to your Sling instance
+* By default the resource provider is _not_ active. You have to enable it via OSGi configuration in the Felix Console (see below)
+
+To create a superimposed resource create a node in JCR with:
+
+* Node type **sling:SuperimposeResource**
+ * Alternatively you can create a node with any other node type and use the mixin **sling:Superimpose**
+* Property **sling:superimposeSourcePath**: points to an absolute path - this content is mirrored to the location of the new node
+* (Optional) Property **sling:superimposeRegisterParent**: If set to true, not the new node itself but its parent is used as root node for the superimposed content. This is useful if you have no control about the parent node itself (e.g. due to node type restrictions).
+* (Optional) Property **sling:superimposeOverlayable**: If set to true, the content is not only mirrored, but can be overlayed by nodes in the target tree below the superimposing root node. _Please note that this feature is still experimental._
+
+
+### Configuration
+
+In the Felix console you can configure the creation of Superimposing Resource Providers via the service "Apache Sling Superimposing Resource Manager":
+
+* **enabled**: If set to true, the superimposing is active
+* **findAllQueries**: Defines JCR queries that are executed on service startup to detect all superimposing nodes that are already created in the JCR. By default only the /content subtree is scanned.
+* **obervationPaths**: Paths on which the new, updated or removed superimposing nodes are automatically detected on runtime.
+
+
+### Remarks
+
+* The superimposing resource provider depends on an underlying JCR repository. It currently does only work with JCR and supports mirroring or overlaying JCR nodes.
+* The Superimposing Resource Provider provides an API in the package org.apache.sling.superimposing. For the basic superimposing content features you do not need this API. It is a read-only API which allows to query which superimposing resource providers are currently active with which configuration. The API is useful if you want to react on JCR events on the source tree and actions on the mirrored trees as well (e.g. for sending invalidation events to clean an external cache).
+* If you want to use the superimposing resource provider within a CMS application that allows to modifiy resource content via it's GUI make sure that this CMS application supports this resource provider in it's authoring environment (and does make direct JCR access, because this bypassed the mirroring and affects the original JCR node - risk of data loss!). If you canne be sure of this please activate the provider only on the sling instances that render the content for the public (publis [...]
+
+
+### Comparison with Sling Resource Merger
+
+In Sling Contrib a ["Apache Sling Resource Merger"](https://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/resourcemerger) bundle is provided. Although both Sling Resource Merger and the Superimposing Resource Provider take care of mirroring and merging resources they solve quite different problems and have different usecases:
+
+* Sling Resource Merger is primary about Merging resources of content structures from /apps and /libs, e.g. dialog definitions of an CMS application. It mounts the merged resources at a new path (e.g. /mnt/overlay) which can be included in the script resolution.
+* The Superimposing Content Resource Provider is targeted at content. Think of a scenario with one master site that is rolled out to hundreds of slave sites with mostly identical contents but some site-specific overrides and customizations. This is not possible with Sling Resource Merger and vice versa.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..30bb123
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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>
+
+    <artifactId>org.apache.sling.superimposing</artifactId>
+    <version>0.2.0-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Superimposing Resource Provider</name>
+    <description>
+        Allows to mirror resource trees and superimposing resources.
+        All changes from the master tree are reflected to the mirrored tree.
+        It is possible to add or overlay resources in the mirrored tree.
+    </description>
+
+    <properties>
+      <sling.url>http://localhost:8080</sling.url>
+      <sling.user>admin</sling.user>
+      <sling.password>admin</sling.password>
+    </properties>
+
+    <build>
+        <plugins>
+
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Export-Package>org.apache.sling.superimposing</Export-Package>
+                        <Private-Package>org.apache.sling.superimposing.impl</Private-Package>
+                        <DynamicImport-Package>org.apache.felix.webconsole</DynamicImport-Package>
+                        <Sling-Nodetypes>SLING-INF/nodetypes/superimposing.cnd</Sling-Nodetypes>
+                    </instructions>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>maven-sling-plugin</artifactId>
+            </plugin>
+
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.settings</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>3.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.9.5</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-nop</artifactId>
+            <version>1.5.2</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>
diff --git a/src/main/java/org/apache/sling/superimposing/SuperimposingManager.java b/src/main/java/org/apache/sling/superimposing/SuperimposingManager.java
new file mode 100644
index 0000000..43d0043
--- /dev/null
+++ b/src/main/java/org/apache/sling/superimposing/SuperimposingManager.java
@@ -0,0 +1,41 @@
+/*
+ * 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.superimposing;
+
+import java.util.Map;
+
+import org.apache.sling.superimposing.impl.SuperimposingResourceProviderImpl;
+
+/**
+ * Manages the resource registrations for the {@link SuperimposingResourceProviderImpl}.
+ * Provides read-only access to all registered providers.
+ */
+public interface SuperimposingManager {
+
+    /**
+     * @return true if superimposing mode is enabled
+     */
+    boolean isEnabled();
+
+    /**
+     * @return Immutable map with all superimposing resource providers currently registered
+     */
+    Map<String, SuperimposingResourceProvider> getRegisteredProviders();
+
+}
diff --git a/src/main/java/org/apache/sling/superimposing/SuperimposingResourceProvider.java b/src/main/java/org/apache/sling/superimposing/SuperimposingResourceProvider.java
new file mode 100644
index 0000000..ac26b37
--- /dev/null
+++ b/src/main/java/org/apache/sling/superimposing/SuperimposingResourceProvider.java
@@ -0,0 +1,66 @@
+/*
+ * 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.superimposing;
+
+import org.apache.sling.api.resource.ResourceProvider;
+
+/**
+ * Superimposing resource provider.
+ * Maps a single source path to the target root path, with or without overlay depending on configuration.
+ */
+public interface SuperimposingResourceProvider extends ResourceProvider {
+
+    /**
+     * Mixin for superimposing.
+     */
+    String MIXIN_SUPERIMPOSE = "sling:Superimpose";
+
+    /**
+     * Property pointing to an absolute or relative repository path, which this superimpose definition points to.
+     */
+    String PROP_SUPERIMPOSE_SOURCE_PATH = "sling:superimposeSourcePath";
+
+    /**
+     * Property indicating if the node itself is used as root for the superimpose definition (default),
+     * of it it's parent should be used. The latter is useful in a Page/PageContent scenario
+     * where the mixin cannot be added on the parent node itself.
+     */
+    String PROP_SUPERIMPOSE_REGISTER_PARENT = "sling:superimposeRegisterParent";
+
+    /**
+     * Property indicating whether this superimposing definition allows the superimposed content
+     * to be overlayed by real nodes created below the superimposing root node.
+     * Default value is false.
+     */
+    String PROP_SUPERIMPOSE_OVERLAYABLE = "sling:superimposeOverlayable";
+
+    /**
+     * @return Root path (source path)
+     */
+    String getRootPath();
+
+    /**
+     * @return Target path (destination path)
+     */
+    String getSourcePath();
+
+    /**
+     * @return Overlayable yes/no
+     */
+    boolean isOverlayable();
+
+}
diff --git a/src/main/java/org/apache/sling/superimposing/impl/SuperimposingManagerImpl.java b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingManagerImpl.java
new file mode 100644
index 0000000..51809ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingManagerImpl.java
@@ -0,0 +1,428 @@
+/*
+ * 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.superimposing.impl;
+
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.MIXIN_SUPERIMPOSE;
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_OVERLAYABLE;
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_REGISTER_PARENT;
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import org.apache.commons.collections.IteratorUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyUnbounded;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.superimposing.SuperimposingManager;
+import org.apache.sling.superimposing.SuperimposingResourceProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the resource registrations for the {@link SuperimposingResourceProviderImpl}.
+ * Provides read-only access to all registered providers.
+ */
+@Component(label = "Apache Sling Superimposing Resource Manager",
+    description = "Manages the resource registrations for the Superimposing Resource Provider.",
+    immediate = true, metatype = true)
+@Service(SuperimposingManager.class)
+public class SuperimposingManagerImpl implements SuperimposingManager, EventListener {
+
+    @Property(label = "Enabled", description = "Enable/Disable the superimposing functionality.", boolValue = SuperimposingManagerImpl.ENABLED_DEFAULT)
+    static final String ENABLED_PROPERTY = "enabled";
+    static final boolean ENABLED_DEFAULT = false;
+    private boolean enabled;
+
+    @Property(label = "Find all Queries", description = "List of query expressions to find all existing superimposing registrations on service startup. "
+            + "Query syntax is depending on underlying resource provdider implementation. Prepend the query with query syntax name separated by \"|\".",
+            value={SuperimposingManagerImpl.FINDALLQUERIES_DEFAULT}, unbounded=PropertyUnbounded.ARRAY)
+    static final String FINDALLQUERIES_PROPERTY = "findAllQueries";
+    static final String FINDALLQUERIES_DEFAULT = "JCR-SQL2|SELECT * FROM [" + MIXIN_SUPERIMPOSE + "] WHERE ISDESCENDANTNODE('/content')";
+    private String[] findAllQueries;
+
+    @Property(label = "Obervation paths", description = "List of paths that should be monitored for resource events to detect superimposing content nodes.",
+            value={SuperimposingManagerImpl.OBSERVATION_PATHS_DEFAULT}, unbounded=PropertyUnbounded.ARRAY)
+    static final String OBSERVATION_PATHS_PROPERTY = "obervationPaths";
+    static final String OBSERVATION_PATHS_DEFAULT = "/content";
+    private String[] obervationPaths;
+    private EventListener[] observationEventListeners;
+
+    /**
+     * Map for holding the superimposing mappings, with the superimpose path as key and the providers as values
+     */
+    private ConcurrentMap<String, SuperimposingResourceProviderImpl> superimposingProviders = new ConcurrentHashMap<String, SuperimposingResourceProviderImpl>();
+
+    @Reference
+    private ResourceResolverFactory resolverFactory;
+
+    /**
+     * Administrative resource resolver (read only usage)
+     */
+    private ResourceResolver resolver;
+
+    /**
+     * A reference to the initialization task. Needed to check if
+     * initialization has completed.
+     */
+    Future<?> initialization;
+
+    /**
+     * This bundle's context.
+     */
+    private BundleContext bundleContext;
+
+    /**
+     * The default logger
+     */
+    private static final Logger log = LoggerFactory.getLogger(SuperimposingManagerImpl.class);
+
+
+    /**
+     * Find all existing superimposing registrations using all query defined in service configuration.
+     * @param resolver Resource resolver
+     * @return All superimposing registrations
+     */
+    @SuppressWarnings("unchecked")
+    private List<Resource> findSuperimposings(ResourceResolver resolver) {
+        List<Resource> allResources = new ArrayList<Resource>();
+        for (String queryString : this.findAllQueries) {
+            if (!StringUtils.contains(queryString, "|")) {
+                throw new IllegalArgumentException("Query string does not contain query syntax seperated by '|': " + queryString);
+            }
+            String queryLanguage = StringUtils.substringBefore(queryString, "|");
+            String query = StringUtils.substringAfter(queryString, "|");
+            allResources.addAll(IteratorUtils.toList(resolver.findResources(query, queryLanguage)));
+        }
+        return allResources;
+    }
+
+    private void registerAllSuperimposings() {
+        log.debug("Start registering all superimposing trees...");
+        final long start = System.currentTimeMillis();
+        long countSuccess = 0;
+        long countFailed = 0;
+
+        final List<Resource> existingSuperimposings = findSuperimposings(resolver);
+        for (Resource superimposingResource : existingSuperimposings) {
+            boolean success = registerProvider(superimposingResource);
+            if (success) {
+                countSuccess++;
+            } else {
+                countFailed++;
+            }
+        }
+
+        final long time = System.currentTimeMillis() - start;
+        log.info("Registered {} SuperimposingResourceProvider(s) in {} ms, skipping {} invalid one(s).",
+                new Object[] { countSuccess, time, countFailed });
+    }
+
+    /**
+     * @param superimposingResource
+     * @return true if registration was done, false if skipped (already registered)
+     * @throws RepositoryException
+     */
+    private boolean registerProvider(Resource superimposingResource) {
+        String superimposePath = superimposingResource.getPath();
+
+        // use JCR API to get properties from superimposing resource to make sure superimposing does not delivery values from source node
+        final String sourcePath = getJcrStringProperty(superimposePath, PROP_SUPERIMPOSE_SOURCE_PATH);
+        final boolean registerParent = getJcrBooleanProperty(superimposePath, PROP_SUPERIMPOSE_REGISTER_PARENT);
+        final boolean overlayable = getJcrBooleanProperty(superimposePath, PROP_SUPERIMPOSE_OVERLAYABLE);
+
+        // check if superimposing definition is valid
+        boolean valid = true;
+        if (StringUtils.isBlank(sourcePath)) {
+            valid = false;
+        }
+        else {
+            // check whether the parent of the node should be registered as superimposing provider
+            if (registerParent) {
+                superimposePath = ResourceUtil.getParent(superimposePath);
+            }
+            // target path is not valid if it equals to a parent or child of the superimposing path, or to the superimposing path itself
+            if (StringUtils.equals(sourcePath, superimposePath)
+                    || StringUtils.startsWith(sourcePath, superimposePath + "/")
+                    || StringUtils.startsWith(superimposePath, sourcePath + "/")) {
+                valid = false;
+            }
+        }
+
+        // register valid superimposing
+        if (valid) {
+            final SuperimposingResourceProviderImpl srp = new SuperimposingResourceProviderImpl(superimposePath, sourcePath, overlayable);
+            final SuperimposingResourceProviderImpl oldSrp = superimposingProviders.put(superimposePath, srp);
+
+            // unregister in case there was a provider registered before
+            if (!srp.equals(oldSrp)) {
+                log.debug("(Re-)registering resource provider {}.", superimposePath);
+                if (null != oldSrp) {
+                    oldSrp.unregisterService();
+                }
+                srp.registerService(bundleContext);
+                return true;
+            } else {
+                log.debug("Skipped re-registering resource provider {} because there were no relevant changes.", superimposePath);
+            }
+        }
+
+        // otherwise remove previous superimposing resource provider if new superimposing definition is not valid
+        else {
+            final SuperimposingResourceProviderImpl oldSrp = superimposingProviders.remove(superimposePath);
+            if (null != oldSrp) {
+                log.debug("Unregistering resource provider {}.", superimposePath);
+                oldSrp.unregisterService();
+            }
+            log.warn("Superimposing definition '{}' pointing to '{}' is invalid.", superimposePath, sourcePath);
+        }
+
+        return false;
+    }
+
+    private String getJcrStringProperty(String pNodePath, String pPropertName) {
+        String absolutePropertyPath = pNodePath + "/" + pPropertName;
+        Session session = resolver.adaptTo(Session.class);
+        try {
+            if (!session.itemExists(absolutePropertyPath)) {
+                return null;
+            }
+            return session.getProperty(absolutePropertyPath).getString();
+        }
+        catch (RepositoryException ex) {
+            return null;
+        }
+    }
+
+    private boolean getJcrBooleanProperty(String pNodePath, String pPropertName) {
+        String absolutePropertyPath = pNodePath + "/" + pPropertName;
+        Session session = resolver.adaptTo(Session.class);
+        try {
+            if (!session.itemExists(absolutePropertyPath)) {
+                return false;
+            }
+            return session.getProperty(absolutePropertyPath).getBoolean();
+        }
+        catch (RepositoryException ex) {
+            return false;
+        }
+    }
+
+    private void registerProvider(String path) {
+        final Resource provider = resolver.getResource(path);
+        if (provider != null) {
+            registerProvider(provider);
+        }
+    }
+
+    private void unregisterProvider(String path) {
+        final SuperimposingResourceProviderImpl srp = superimposingProviders.remove(path);
+        if (null != srp) {
+            srp.unregisterService();
+        }
+    }
+
+    // ---------- SCR Integration
+
+    @Activate
+    protected synchronized void activate(final ComponentContext ctx) throws LoginException, RepositoryException {
+
+        // check enabled state
+        @SuppressWarnings("unchecked")
+        final Dictionary<String, Object> props = ctx.getProperties();
+        this.enabled = PropertiesUtil.toBoolean(props.get(ENABLED_PROPERTY), ENABLED_DEFAULT);
+        log.debug("Config: " + "Enabled={} ", enabled);
+        if (!isEnabled()) {
+            return;
+        }
+
+        // get "find all" queries
+        this.findAllQueries = PropertiesUtil.toStringArray(props.get(FINDALLQUERIES_PROPERTY), new String[] { FINDALLQUERIES_DEFAULT });
+        this.obervationPaths = PropertiesUtil.toStringArray(props.get(OBSERVATION_PATHS_PROPERTY), new String[] { OBSERVATION_PATHS_DEFAULT });
+
+        if (null == resolver) {
+            bundleContext = ctx.getBundleContext();
+            resolver = resolverFactory.getAdministrativeResourceResolver(null);
+
+            // Watch for events on the root to register/deregister superimposings at runtime
+            // For each observed path create an event listener object which redirects the event to the main class
+            final Session session = resolver.adaptTo(Session.class);
+            if (session!=null) {
+                this.observationEventListeners = new EventListener[this.obervationPaths.length];
+                for (int i=0; i<this.obervationPaths.length; i++) {
+                    this.observationEventListeners[i] = new EventListener() {
+                        public void onEvent(EventIterator events) {
+                            SuperimposingManagerImpl.this.onEvent(events);
+                        }
+                    };
+                    session.getWorkspace().getObservationManager().addEventListener(
+                            this.observationEventListeners[i],
+                            Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED,
+                            this.obervationPaths[i], // absolute path
+                            true, // isDeep
+                            null, // uuids
+                            null, // node types
+                            true); // noLocal
+                }
+            }
+
+            // register all superimposing definitions that already exist
+            initialization = Executors.newSingleThreadExecutor().submit(new Runnable() {
+                public void run() {
+                    try {
+                        registerAllSuperimposings();
+                    }
+                    catch (Throwable ex) {
+                        log.warn("Error registering existing superimposing resources on service startup.", ex);
+                    }
+                }
+            });
+        }
+    }
+
+    @Deactivate
+    protected synchronized void deactivate(final ComponentContext ctx) throws RepositoryException {
+        try {
+            // make sure initialization has finished
+            if (null != initialization && !initialization.isDone()) {
+                initialization.cancel(/* myInterruptIfRunning */ true);
+            }
+
+            // de-register JCR observation
+            if (resolver!=null) {
+                final Session session = resolver.adaptTo(Session.class);
+                if (session!=null && this.observationEventListeners!=null) {
+                    for (EventListener eventListener : this.observationEventListeners) {
+                        session.getWorkspace().getObservationManager().removeEventListener(eventListener);
+                    }
+                }
+            }
+
+            // de-register all superimpsing resource providers
+            for (final SuperimposingResourceProviderImpl srp : superimposingProviders.values()) {
+                srp.unregisterService();
+            }
+
+        } finally {
+            if (null != resolver) {
+                resolver.close();
+                resolver = null;
+            }
+            initialization = null;
+            superimposingProviders.clear();
+        }
+    }
+
+    /**
+     * Handle resource events to add or remove superimposing registrations
+     */
+    public void onEvent(EventIterator events) {
+        if (!isEnabled()) {
+            return;
+        }
+        try {
+            // collect all actions to be performed for this event
+            final Map<String, Boolean> actions = new HashMap<String, Boolean>();
+            boolean nodeAdded = false;
+            boolean nodeRemoved = false;
+            while (events.hasNext()) {
+                final Event event = events.nextEvent();
+                final String path = event.getPath();
+                final String name = ResourceUtil.getName(path);
+                if (event.getType() == Event.NODE_ADDED) {
+                    nodeAdded = true;
+                } else if (event.getType() == Event.NODE_REMOVED && superimposingProviders.containsKey(path)) {
+                    nodeRemoved = true;
+                    actions.put(path, false);
+                } else if (StringUtils.equals(name, PROP_SUPERIMPOSE_SOURCE_PATH)
+                        || StringUtils.equals(name, PROP_SUPERIMPOSE_REGISTER_PARENT)
+                        || StringUtils.equals(name, PROP_SUPERIMPOSE_OVERLAYABLE)) {
+                    final String nodePath = ResourceUtil.getParent(path);
+                    actions.put(nodePath, true);
+                }
+            }
+
+            // execute all collected actions (having this outside the above
+            // loop prevents repeated registrations within one transaction
+            // but allows for several superimposings to be added within a single
+            // transaction)
+            for (Map.Entry<String, Boolean> action : actions.entrySet()) {
+                if (action.getValue()) {
+                    registerProvider(action.getKey());
+                } else {
+                    unregisterProvider(action.getKey());
+                }
+            }
+
+            if (nodeAdded && nodeRemoved) {
+                // maybe a superimposing was moved, re-register all superimposings
+                // (existing ones will be skipped)
+                registerAllSuperimposings();
+            }
+        } catch (RepositoryException e) {
+            log.error("Unexpected repository exception during event processing.");
+        }
+    }
+
+    /**
+     * @return true if superimposing mode is enabled
+     */
+    public boolean isEnabled() {
+        return this.enabled;
+    }
+
+    /**
+     * @return Immutable map with all superimposing resource providers currently registered
+     */
+    public Map<String, SuperimposingResourceProvider> getRegisteredProviders() {
+        Map<String, SuperimposingResourceProvider> mapcopy = new HashMap<String, SuperimposingResourceProvider>(superimposingProviders);
+        return Collections.unmodifiableMap(mapcopy);
+    }
+
+    SuperimposingManagerImpl withResourceResolverFactory(ResourceResolverFactory resolverFactory) {
+        this.resolverFactory = resolverFactory;
+        return this;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResource.java b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResource.java
new file mode 100644
index 0000000..9f1fd7a
--- /dev/null
+++ b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResource.java
@@ -0,0 +1,92 @@
+/*
+ * 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.superimposing.impl;
+
+import org.apache.sling.api.resource.AbstractResource;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+
+/**
+ * {@link SuperimposingResource} is provided by {@link SuperimposingResourceProviderImpl} instances.
+ * It delegates to an existing underlying resource but overrides the getPath() method to point to the superimposed path.
+ */
+public class SuperimposingResource extends AbstractResource implements Resource {
+
+    private final Resource resource;
+    private final ResourceMetadata resourceMetadata;
+    private final String path;
+
+    /**
+     * @param mappedResource Mapped resource
+     * @param path Path
+     */
+    public SuperimposingResource(Resource mappedResource, String path) {
+        this.resource = mappedResource;
+
+        // make a copy of resource metadata object
+        this.resourceMetadata = new ResourceMetadata();
+        if (mappedResource.getResourceMetadata()!=null) {
+            this.resourceMetadata.putAll(mappedResource.getResourceMetadata());
+        }
+
+        this.path = path;
+    }
+
+    public String getPath() {
+        return this.path;
+    }
+
+    public String getResourceType() {
+        return resource.getResourceType();
+    }
+
+    public String getResourceSuperType() {
+        return resource.getResourceSuperType();
+    }
+
+    public ResourceMetadata getResourceMetadata() {
+        return this.resourceMetadata;
+    }
+
+    public ResourceResolver getResourceResolver() {
+        return resource.getResourceResolver();
+    }
+
+    @Override
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        AdapterType adapted = super.adaptTo(type);
+        if (null == adapted) {
+            // fallback to adapt from mapped resource (although this may lead sometimes to unexpected results e.g. original JCR node)
+            adapted = resource.adaptTo(type);
+        }
+        return adapted;
+    }
+
+    Resource getResource() {
+        return this.resource;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder(getClass().getSimpleName())
+                .append("[type=").append(getResourceType())
+                .append(", path=").append(getPath())
+                .append(", resource=[").append(getResource()).append("]]").toString();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResourceIterator.java b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResourceIterator.java
new file mode 100644
index 0000000..1077b9b
--- /dev/null
+++ b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResourceIterator.java
@@ -0,0 +1,69 @@
+/*
+ * 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.superimposing.impl;
+
+import org.apache.sling.api.resource.Resource;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterates over children of a {@link SuperimposingResource} by iterating over the children of the source resource
+ * and remapping and wrapping all children in SuperimposingResource instances.
+ */
+public class SuperimposingResourceIterator implements Iterator<Resource> {
+
+    private final SuperimposingResourceProviderImpl superimposingProvider;
+    private final Iterator<Resource> decoratee;
+
+    private Resource next;
+
+    SuperimposingResourceIterator(SuperimposingResourceProviderImpl superimposingProvider, Iterator<Resource> decoratee) {
+        this.superimposingProvider = superimposingProvider;
+        this.decoratee = decoratee;
+        seek();
+    }
+
+    private void seek() {
+        next = null;
+        while (next == null && decoratee.hasNext()) {
+            final Resource resource = decoratee.next();
+            final String superimposingPath = SuperimposingResourceProviderImpl.reverseMapPath(superimposingProvider, resource.getPath());
+            if (null != superimposingPath) {
+                next = new SuperimposingResource(resource, superimposingPath);
+            }
+        }
+    }
+
+    public boolean hasNext() {
+        return null != next;
+    }
+
+    public Resource next() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+        final Resource current = next;
+        seek();
+        return current;
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResourceProviderImpl.java b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResourceProviderImpl.java
new file mode 100644
index 0000000..7751c29
--- /dev/null
+++ b/src/main/java/org/apache/sling/superimposing/impl/SuperimposingResourceProviderImpl.java
@@ -0,0 +1,259 @@
+/*
+ * 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.superimposing.impl;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Iterator;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceWrapper;
+import org.apache.sling.superimposing.SuperimposingResourceProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Superimposing resource provider.
+ * Maps a single source path to the target root path, with or without overlay depending on configuration.
+ */
+public class SuperimposingResourceProviderImpl implements SuperimposingResourceProvider {
+
+    private static final Logger log = LoggerFactory.getLogger(SuperimposingResourceProviderImpl.class);
+
+    private final String rootPath;
+    private final String rootPrefix;
+    private final String sourcePath;
+    private final String sourcePathPrefix;
+    private final boolean overlayable;
+    private final String toString;
+    private ServiceRegistration registration;
+
+    SuperimposingResourceProviderImpl(String rootPath, String sourcePath, boolean overlayable) {
+        this.rootPath = rootPath;
+        this.rootPrefix = rootPath.concat("/");
+        this.sourcePath = sourcePath;
+        this.sourcePathPrefix = sourcePath.concat("/");
+        this.overlayable = overlayable;
+        StringBuilder sb = new StringBuilder(getClass().getSimpleName());
+        sb.append(" [path=").append(rootPath).append(", ");
+        sb.append("sourcePath=").append(sourcePath).append(", ");
+        sb.append("overlayable=").append(overlayable).append("]");
+        this.toString = sb.toString();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Resource getResource(ResourceResolver resolver, HttpServletRequest httpServletRequest, String path) {
+        return getResource(resolver, path);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Resource getResource(ResourceResolver resolver, String path) {
+        final String mappedPath = mapPath(this, resolver, path);
+        if (null != mappedPath) {
+            // the existing resource where the superimposed content is retrieved from
+            final Resource mappedResource = resolver.getResource(mappedPath);
+            if (null != mappedResource) {
+                return new SuperimposingResource(mappedResource, path);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Iterator<Resource> listChildren(Resource resource) {
+
+        // unwrap resource if it is a wrapped resource
+        final Resource currentResource;
+        if (resource instanceof ResourceWrapper) {
+            currentResource = ((ResourceWrapper)resource).getResource();
+        }
+        else {
+            currentResource = resource;
+        }
+
+        // delegate resource listing to resource resolver
+        if (currentResource instanceof SuperimposingResource) {
+            final SuperimposingResource res = (SuperimposingResource) currentResource;
+            final ResourceResolver resolver = res.getResource().getResourceResolver();
+            final Iterator<Resource> children = resolver.listChildren(res.getResource());
+            return new SuperimposingResourceIterator(this, children);
+        }
+        return null;
+    }
+
+    /**
+     * Maps a path below the superimposing root to the target resource's path.
+     * @param provider Superimposing resource provider
+     * @param resolver Resource resolver
+     * @param path Path to map
+     * @return Mapped path or null if no mapping available
+     */
+    static String mapPath(SuperimposingResourceProviderImpl provider, ResourceResolver resolver, String path) {
+        if (provider.overlayable) {
+            return mapPathWithOverlay(provider, resolver, path);
+        }
+        else {
+            return mapPathWithoutOverlay(provider, resolver, path);
+        }
+    }
+
+    /**
+     * Maps a path below the superimposing root to the target resource's path with check for overlaying.
+     * @param provider Superimposing resource provider
+     * @param resolver Resource resolver
+     * @param path Path to map
+     * @return Mapped path or null if no mapping available
+     */
+    static String mapPathWithOverlay(SuperimposingResourceProviderImpl provider, ResourceResolver resolver, String path) {
+        if (StringUtils.equals(path, provider.rootPath)) {
+            // Superimposing root path cannot be overlayed
+            return mapPathWithoutOverlay(provider, resolver, path);
+        }
+        else if (StringUtils.startsWith(path, provider.rootPrefix)) {
+            if (hasOverlayResource(resolver, path)) {
+                // overlay item exists, allow underlying resource provider to step in
+                return null;
+            }
+            else {
+                // overlay item does not exist, overlay cannot be applied, fallback to mapped path without overlay
+                return mapPathWithoutOverlay(provider, resolver, path);
+            }
+        }
+        return null;
+    }
+
+    static boolean hasOverlayResource(ResourceResolver resolver, String path) {
+        // check for overlay resource by checking directly in underlying JCR
+        final Session session = resolver.adaptTo(Session.class);
+        try {
+            return (null != session && session.itemExists(path));
+        } catch (RepositoryException e) {
+            log.error("Error accessing the repository. ", e);
+        }
+        return false;
+    }
+
+    /**
+     * Maps a path below the superimposing root to the target resource's path without check for overlaying.
+     * @param provider Superimposing resource provider
+     * @param resolver Resource resolver
+     * @param path Path to map
+     * @return Mapped path or null if no mapping available
+     */
+    static String mapPathWithoutOverlay(SuperimposingResourceProviderImpl provider, ResourceResolver resolver, String path) {
+        final String mappedPath;
+        if (StringUtils.equals(path, provider.rootPath)) {
+            mappedPath = provider.sourcePath;
+        } else if (StringUtils.startsWith(path, provider.rootPrefix)) {
+            mappedPath = StringUtils.replaceOnce(path, provider.rootPrefix, provider.sourcePathPrefix);
+        } else {
+            mappedPath = null;
+        }
+        return mappedPath;
+    }
+
+    /**
+     * Maps a path below the target resource to the superimposed resource's path.
+     *
+     * @param provider
+     * @param path
+     * @return
+     */
+    static String reverseMapPath(SuperimposingResourceProviderImpl provider, String path) {
+        final String mappedPath;
+        if (path.startsWith(provider.sourcePathPrefix)) {
+            mappedPath = StringUtils.replaceOnce(path, provider.sourcePathPrefix, provider.rootPrefix);
+        } else if (path.equals(provider.sourcePath)) {
+            mappedPath = provider.rootPath;
+        } else {
+            mappedPath = null;
+        }
+        return mappedPath;
+    }
+
+    //---------- Service Registration
+
+    void registerService(BundleContext context) {
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(Constants.SERVICE_DESCRIPTION, "Provider of superimposed resources");
+        props.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
+        props.put(ROOTS, new String[]{rootPath});
+
+        registration = context.registerService(SERVICE_NAME, this, props);
+
+        log.info("Registered {}", this);
+    }
+
+    void unregisterService() {
+        if (registration != null) {
+            registration.unregister();
+            registration = null;
+            log.info("Unregistered {}", this);
+        }
+    }
+
+    /**
+     * @return Root path (source path)
+     */
+    public String getRootPath() {
+        return rootPath;
+    }
+
+    /**
+     * @return Target path (destination path)
+     */
+    public String getSourcePath() {
+        return sourcePath;
+    }
+
+    /**
+     * @return Overlayable yes/no
+     */
+    public boolean isOverlayable() {
+        return overlayable;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof SuperimposingResourceProviderImpl) {
+            final SuperimposingResourceProviderImpl srp = (SuperimposingResourceProviderImpl)o;
+            return this.rootPath.equals(srp.rootPath) && this.sourcePath.equals(srp.sourcePath) && this.overlayable == srp.overlayable;
+
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return toString;
+    }
+}
diff --git a/src/main/resources/SLING-INF/nodetypes/superimposing.cnd b/src/main/resources/SLING-INF/nodetypes/superimposing.cnd
new file mode 100644
index 0000000..7a94b02
--- /dev/null
+++ b/src/main/resources/SLING-INF/nodetypes/superimposing.cnd
@@ -0,0 +1,45 @@
+//
+//  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.
+//
+
+<sling = 'http://sling.apache.org/jcr/sling/1.0'>
+
+//-----------------------------------------------------------------------------
+// Mixin node type to allow creating superimposing nodes
+[sling:Superimpose]
+    mixin
+
+    // mandatory property pointing to an absolute or relative
+    // repository path, which this node should superimpose
+  - sling:superimposeSourcePath (string) mandatory
+
+    // optional property indicating if the node itself is used
+    // as root for the superimposing definition (default), of it it's parent should
+    // be used. The latter is useful in a Page/PageContent scenario
+    // where the mixin cannot be added on the parent node itself.  
+  - sling:superimposeRegisterParent (boolean)
+
+    // optional property indicating whether this superimposing definition allows
+    // the superimposed content to be overlayed by real nodes
+    // created below the superimposing root node  
+  - sling:superimposeOverlayable (boolean)
+
+
+//-----------------------------------------------------------------------------
+// Convenience node type for creating superimposing nodes
+[sling:SuperimposeResource] > sling:Superimpose, nt:unstructured
diff --git a/src/test/java/org/apache/sling/superimposing/impl/SuperimposingManagerImplTest.java b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingManagerImplTest.java
new file mode 100644
index 0000000..c4caea3
--- /dev/null
+++ b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingManagerImplTest.java
@@ -0,0 +1,375 @@
+/*
+ * 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.superimposing.impl;
+
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_OVERLAYABLE;
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_REGISTER_PARENT;
+import static org.apache.sling.superimposing.SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.superimposing.SuperimposingResourceProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SuperimposingManagerImplTest {
+
+    @Mock
+    private Dictionary<String, Object> componentContextProperties;
+    @Mock
+    private ComponentContext componentContext;
+    @Mock
+    private BundleContext bundleContext;
+    @Mock
+    private ResourceResolverFactory resourceResolverFactory;
+    @Mock
+    private ResourceResolver resourceResolver;
+    @Mock(answer=Answers.RETURNS_DEEP_STUBS)
+    private Session session;
+    private List<ServiceRegistration> serviceRegistrations = new ArrayList<ServiceRegistration>();
+
+    private SuperimposingManagerImpl underTest;
+
+    private static final String ORIGINAL_PATH = "/root/path1";
+    private static final String SUPERIMPOSED_PATH = "/root/path2";
+    private static final String OBSERVATION_PATH = "/root";
+
+    @SuppressWarnings("unchecked")
+    @Before
+    public void setUp() throws LoginException {
+        when(componentContext.getBundleContext()).thenReturn(bundleContext);
+        when(componentContext.getProperties()).thenReturn(componentContextProperties);
+        when(componentContextProperties.get(SuperimposingManagerImpl.OBSERVATION_PATHS_PROPERTY)).thenReturn(new String[] { OBSERVATION_PATH });
+        when(resourceResolverFactory.getAdministrativeResourceResolver(any(Map.class))).thenReturn(resourceResolver);
+        when(resourceResolver.adaptTo(Session.class)).thenReturn(session);
+
+        // collect a list of all service registrations to validate that they are all unregistered on shutdown
+        when(bundleContext.registerService(anyString(), anyObject(), any(Dictionary.class))).thenAnswer(new Answer<ServiceRegistration>() {
+            public ServiceRegistration answer(InvocationOnMock invocation) {
+                final ServiceRegistration mockRegistration = mock(ServiceRegistration.class);
+                serviceRegistrations.add(mockRegistration);
+                doAnswer(new Answer() {
+                    public Object answer(InvocationOnMock invocation) {
+                        return serviceRegistrations.remove(mockRegistration);
+                    }
+                }).when(mockRegistration).unregister();
+                return mockRegistration;
+            }
+        });
+
+        // simulate absolute path access to properties via session object
+        try {
+            when(session.itemExists(anyString())).thenAnswer(new Answer<Boolean>() {
+                public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                    final String absolutePath = (String)invocation.getArguments()[0];
+                    final String nodePath = ResourceUtil.getParent(absolutePath);
+                    final String propertyName = ResourceUtil.getName(absolutePath);
+                    Resource resource = resourceResolver.getResource(nodePath);
+                    if (resource!=null) {
+                        ValueMap props = resource.adaptTo(ValueMap.class);
+                        return props.containsKey(propertyName);
+                    }
+                    else {
+                        return false;
+                    }
+                }
+            });
+            when(session.getProperty(anyString())).thenAnswer(new Answer<Property>() {
+                public Property answer(InvocationOnMock invocation) throws Throwable {
+                    final String absolutePath = (String)invocation.getArguments()[0];
+                    final String nodePath = ResourceUtil.getParent(absolutePath);
+                    final String propertyName = ResourceUtil.getName(absolutePath);
+                    Resource resource = resourceResolver.getResource(nodePath);
+                    if (resource!=null) {
+                        ValueMap props = resource.adaptTo(ValueMap.class);
+                        Object value = props.get(propertyName);
+                        if (value==null) {
+                            throw new PathNotFoundException();
+                        }
+                        Property prop = mock(Property.class);
+                        when(prop.getName()).thenReturn(propertyName);
+                        if (value instanceof String) {
+                            when(prop.getString()).thenReturn((String)value);
+                        }
+                        else if (value instanceof Boolean) {
+                            when(prop.getBoolean()).thenReturn((Boolean)value);
+                        }
+                        return prop;
+                    }
+                    else {
+                        throw new PathNotFoundException();
+                    }
+                }
+            });
+        }
+        catch (RepositoryException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private void initialize(boolean enabled) throws InterruptedException, LoginException, RepositoryException {
+        when(componentContextProperties.get(SuperimposingManagerImpl.ENABLED_PROPERTY)).thenReturn(enabled);
+
+        underTest = new SuperimposingManagerImpl().withResourceResolverFactory(resourceResolverFactory);
+        underTest.activate(componentContext);
+
+        if (underTest.isEnabled()) {
+            // verify observation registration
+            verify(session.getWorkspace().getObservationManager()).addEventListener(any(EventListener.class), anyInt(), eq(OBSERVATION_PATH), anyBoolean(), any(String[].class), any(String[].class), anyBoolean());
+            // wait until separate initialization thread has finished
+            while (!underTest.initialization.isDone()) {
+                Thread.sleep(10);
+            }
+        }
+    }
+
+    @After
+    public void tearDown() throws RepositoryException {
+        underTest.deactivate(componentContext);
+
+        if (underTest.isEnabled()) {
+            // verify observation and resource resolver are terminated correctly
+            verify(session.getWorkspace().getObservationManager()).removeEventListener(any(EventListener.class));
+            verify(resourceResolver).close();
+        }
+
+        // make sure all registrations are unregistered on shutdown
+        for (ServiceRegistration registration : serviceRegistrations) {
+            verify(registration, times(1)).unregister();
+        }
+    }
+
+    private Resource prepareSuperimposingResource(String superimposedPath, String sourcePath, boolean registerParent, boolean overlayable) {
+        Resource resource = mock(Resource.class);
+        when(resource.getPath()).thenReturn(superimposedPath);
+        ValueMap props = new ValueMapDecorator(new HashMap<String, Object>());
+        props.put(PROP_SUPERIMPOSE_SOURCE_PATH, sourcePath);
+        props.put(PROP_SUPERIMPOSE_REGISTER_PARENT, registerParent);
+        props.put(PROP_SUPERIMPOSE_OVERLAYABLE, overlayable);
+        when(resource.adaptTo(ValueMap.class)).thenReturn(props);
+        when(resourceResolver.getResource(superimposedPath)).thenReturn(resource);
+        return resource;
+    }
+
+    private void moveSuperimposedResource(Resource resource, String newPath) {
+        String oldPath = resource.getPath();
+        when(resource.getPath()).thenReturn(newPath);
+        when(resourceResolver.getResource(oldPath)).thenReturn(null);
+        when(resourceResolver.getResource(newPath)).thenReturn(resource);
+    }
+
+    @Test
+    public void testDisabled() throws InterruptedException, LoginException, RepositoryException {
+        // make sure that no exception is thrown when service is disabled on activate/deactivate
+        initialize(false);
+
+        verifyZeroInteractions(resourceResolverFactory);
+        verifyZeroInteractions(bundleContext);
+    }
+
+    @Test
+    public void testFindAllSuperimposings() throws InterruptedException, LoginException, RepositoryException {
+        // prepare a query that returns one existing superimposed resource
+        when(componentContextProperties.get(SuperimposingManagerImpl.FINDALLQUERIES_PROPERTY)).thenReturn("syntax|query");
+        when(resourceResolver.findResources("query", "syntax")).then(new Answer<Iterator<Resource>>() {
+            public Iterator<Resource> answer(InvocationOnMock invocation) {
+                return Arrays.asList(new Resource[] {
+                        prepareSuperimposingResource(SUPERIMPOSED_PATH, ORIGINAL_PATH, false, false)
+                }).iterator();
+            }
+        });
+        initialize(true);
+
+        // ensure the superimposed resource is detected and registered
+        Map<String, SuperimposingResourceProvider> providers = underTest.getRegisteredProviders();
+        assertEquals(1, providers.size());
+        SuperimposingResourceProvider provider = providers.values().iterator().next();
+        assertEquals(SUPERIMPOSED_PATH, provider.getRootPath());
+        assertEquals(ORIGINAL_PATH, provider.getSourcePath());
+        assertFalse(provider.isOverlayable());
+        verify(bundleContext).registerService(anyString(), same(provider), any(Dictionary.class));
+    }
+
+    private EventIterator prepareNodeCreateEvent(Resource pResource) throws RepositoryException {
+        String resourcePath = pResource.getPath();
+
+        Event nodeEvent = mock(Event.class);
+        when(nodeEvent.getType()).thenReturn(Event.NODE_ADDED);
+        when(nodeEvent.getPath()).thenReturn(resourcePath);
+
+        Event propertyEvent = mock(Event.class);
+        when(propertyEvent.getType()).thenReturn(Event.PROPERTY_ADDED);
+        when(propertyEvent.getPath()).thenReturn(resourcePath + "/" + SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH);
+
+        EventIterator eventIterator = mock(EventIterator.class);
+        when(eventIterator.hasNext()).thenReturn(true, true, false);
+        when(eventIterator.nextEvent()).thenReturn(nodeEvent, propertyEvent);
+        return eventIterator;
+    }
+
+    private EventIterator prepareNodeChangeEvent(Resource pResource) throws RepositoryException {
+        String resourcePath = pResource.getPath();
+
+        Event propertyEvent = mock(Event.class);
+        when(propertyEvent.getType()).thenReturn(Event.PROPERTY_CHANGED);
+        when(propertyEvent.getPath()).thenReturn(resourcePath + "/" + SuperimposingResourceProvider.PROP_SUPERIMPOSE_SOURCE_PATH);
+
+        EventIterator eventIterator = mock(EventIterator.class);
+        when(eventIterator.hasNext()).thenReturn(true, false);
+        when(eventIterator.nextEvent()).thenReturn(propertyEvent);
+        return eventIterator;
+    }
+
+    private EventIterator prepareNodeRemoveEvent(Resource pResource) throws RepositoryException {
+        String resourcePath = pResource.getPath();
+
+        Event nodeEvent = mock(Event.class);
+        when(nodeEvent.getType()).thenReturn(Event.NODE_REMOVED);
+        when(nodeEvent.getPath()).thenReturn(resourcePath);
+
+        EventIterator eventIterator = mock(EventIterator.class);
+        when(eventIterator.hasNext()).thenReturn(true, false);
+        when(eventIterator.nextEvent()).thenReturn(nodeEvent);
+        return eventIterator;
+    }
+
+    private EventIterator prepareNodeMoveEvent(Resource pResource, String pOldPath) throws RepositoryException {
+        String resourcePath = pResource.getPath();
+
+        Event nodeRemoveEvent = mock(Event.class);
+        when(nodeRemoveEvent.getType()).thenReturn(Event.NODE_REMOVED);
+        when(nodeRemoveEvent.getPath()).thenReturn(pOldPath);
+
+        Event nodeCreateEvent = mock(Event.class);
+        when(nodeCreateEvent.getType()).thenReturn(Event.NODE_ADDED);
+        when(nodeCreateEvent.getPath()).thenReturn(resourcePath);
+
+        EventIterator eventIterator = mock(EventIterator.class);
+        when(eventIterator.hasNext()).thenReturn(true, true, false);
+        when(eventIterator.nextEvent()).thenReturn(nodeRemoveEvent, nodeCreateEvent);
+        return eventIterator;
+    }
+
+    @Test
+    public void testSuperimposedResourceCreateUpdateRemove() throws InterruptedException, LoginException, RepositoryException {
+        initialize(true);
+
+        // simulate node create event
+        Resource superimposedResource = prepareSuperimposingResource(SUPERIMPOSED_PATH, ORIGINAL_PATH, false, false);
+        underTest.onEvent(prepareNodeCreateEvent(superimposedResource));
+
+        // ensure the superimposed resource is detected and registered
+        Map<String, SuperimposingResourceProvider> providers = underTest.getRegisteredProviders();
+        assertEquals(1, providers.size());
+        SuperimposingResourceProvider provider = providers.values().iterator().next();
+        assertEquals(SUPERIMPOSED_PATH, provider.getRootPath());
+        assertEquals(ORIGINAL_PATH, provider.getSourcePath());
+        assertFalse(provider.isOverlayable());
+        verify(bundleContext).registerService(anyString(), same(provider), any(Dictionary.class));
+
+        // simulate a change in the original path
+        superimposedResource.adaptTo(ValueMap.class).put(PROP_SUPERIMPOSE_SOURCE_PATH, "/other/path");
+        underTest.onEvent(prepareNodeChangeEvent(superimposedResource));
+
+        // ensure the superimposed resource update is detected and a new provider instance is registered
+        providers = underTest.getRegisteredProviders();
+        assertEquals(1, providers.size());
+        SuperimposingResourceProvider provider2 = providers.values().iterator().next();
+        assertEquals(SUPERIMPOSED_PATH, provider2.getRootPath());
+        assertEquals("/other/path", provider2.getSourcePath());
+        assertFalse(provider2.isOverlayable());
+        verify(bundleContext).registerService(anyString(), same(provider2), any(Dictionary.class));
+
+        // simulate node removal
+        underTest.onEvent(prepareNodeRemoveEvent(superimposedResource));
+
+        // ensure provider is removed
+        providers = underTest.getRegisteredProviders();
+        assertEquals(0, providers.size());
+    }
+
+    @Test
+    public void testSuperimposedResourceCreateMove() throws InterruptedException, LoginException, RepositoryException {
+        when(componentContextProperties.get(SuperimposingManagerImpl.FINDALLQUERIES_PROPERTY)).thenReturn("syntax|query");
+        initialize(true);
+
+        // simulate node create event
+        final Resource superimposedResource = prepareSuperimposingResource(SUPERIMPOSED_PATH, ORIGINAL_PATH, false, false);
+        underTest.onEvent(prepareNodeCreateEvent(superimposedResource));
+
+        // simulate a node move event
+        String oldPath = superimposedResource.getPath();
+        moveSuperimposedResource(superimposedResource, "/new/path");
+
+        // prepare a query that returns the moved superimposed resource
+        when(resourceResolver.findResources("query", "syntax")).then(new Answer<Iterator<Resource>>() {
+            public Iterator<Resource> answer(InvocationOnMock invocation) {
+                return Arrays.asList(new Resource[] {
+                        superimposedResource
+                }).iterator();
+            }
+        });
+
+        underTest.onEvent(prepareNodeMoveEvent(superimposedResource, oldPath));
+
+        // ensure the superimposed resource update is detected and a new provider instance is registered
+        Map<String, SuperimposingResourceProvider> providers = underTest.getRegisteredProviders();
+        assertEquals(1, providers.size());
+        SuperimposingResourceProvider provider = providers.values().iterator().next();
+        assertEquals("/new/path", provider.getRootPath());
+        assertEquals(ORIGINAL_PATH, provider.getSourcePath());
+        assertFalse(provider.isOverlayable());
+        verify(bundleContext).registerService(anyString(), same(provider), any(Dictionary.class));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceIteratorTest.java b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceIteratorTest.java
new file mode 100644
index 0000000..96d3209
--- /dev/null
+++ b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceIteratorTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.superimposing.impl;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.apache.sling.api.resource.Resource;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SuperimposingResourceIteratorTest {
+
+    @Mock
+    private Iterator<Resource> originalResourceIterator;
+    @Mock
+    private Resource originalResource1;
+    @Mock
+    private Resource originalResource2;
+
+    private SuperimposingResourceProviderImpl superimposingResourceProvider;
+
+    private static final String ORIGINAL_PATH = "/root/path1";
+    private static final String SUPERIMPOSED_PATH = "/root/path2";
+
+    @Before
+    public void setUp() {
+        this.superimposingResourceProvider = new SuperimposingResourceProviderImpl(SUPERIMPOSED_PATH, ORIGINAL_PATH, false);
+        when(this.originalResource1.getPath()).thenReturn(ORIGINAL_PATH + "/node1");
+        when(this.originalResource2.getPath()).thenReturn(ORIGINAL_PATH + "/node2");
+    }
+
+    @Test
+    public void testEmpty() {
+        when(this.originalResourceIterator.hasNext()).thenReturn(false);
+        Iterator<Resource> underTest = new SuperimposingResourceIterator(this.superimposingResourceProvider, this.originalResourceIterator);
+        assertFalse(underTest.hasNext());
+    }
+
+    @Test(expected=NoSuchElementException.class)
+    public void testEmptyGetNext() {
+        when(this.originalResourceIterator.hasNext()).thenReturn(false);
+        Iterator<Resource> underTest = new SuperimposingResourceIterator(this.superimposingResourceProvider, this.originalResourceIterator);
+        underTest.next();
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testRemove() {
+        Iterator<Resource> underTest = new SuperimposingResourceIterator(this.superimposingResourceProvider, this.originalResourceIterator);
+        underTest.remove();
+    }
+
+    @Test
+    public void testWith2Elements() {
+        when(this.originalResourceIterator.hasNext()).thenReturn(true, true, false);
+        when(this.originalResourceIterator.next()).thenReturn(this.originalResource1, this.originalResource2, null);
+        Iterator<Resource> underTest = new SuperimposingResourceIterator(this.superimposingResourceProvider, this.originalResourceIterator);
+
+        assertTrue(underTest.hasNext());
+        assertEquals(SUPERIMPOSED_PATH + "/node1", underTest.next().getPath());
+
+        assertTrue(underTest.hasNext());
+        assertEquals(SUPERIMPOSED_PATH + "/node2", underTest.next().getPath());
+
+        assertFalse(underTest.hasNext());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceProviderImplTest.java b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceProviderImplTest.java
new file mode 100644
index 0000000..fb2e4f2
--- /dev/null
+++ b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceProviderImplTest.java
@@ -0,0 +1,241 @@
+/*
+ * 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.superimposing.impl;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Iterator;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceWrapper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SuperimposingResourceProviderImplTest {
+
+    private SuperimposingResourceProviderImpl underTest;
+    private SuperimposingResourceProviderImpl underTestOverlay;
+
+    @Mock
+    private BundleContext bundleContext;
+    @Mock
+    private ServiceRegistration serviceRegistration;
+    @Mock
+    private ServiceRegistration serviceRegistrationOverlay;
+    @Mock
+    private ResourceResolver resourceResolver;
+    @Mock
+    private Session session;
+    @Mock
+    private Resource originalRootResource;
+    @Mock
+    private Resource originalSubResource;
+
+    private static final String ORIGINAL_PATH = "/root/path1";
+    private static final String SUPERIMPOSED_PATH = "/root/path2";
+    private static final String RESOURCE_TYPE = "/resourceType1";
+
+    @Before
+    public void setUp() {
+        // setup a superimposing resource provider without overlay
+        underTest = new SuperimposingResourceProviderImpl(SUPERIMPOSED_PATH, ORIGINAL_PATH, false);
+        when(bundleContext.registerService(anyString(), eq(underTest), any(Dictionary.class))).thenReturn(serviceRegistration);
+        underTest.registerService(bundleContext);
+
+        // and one with overlay
+        underTestOverlay = new SuperimposingResourceProviderImpl(SUPERIMPOSED_PATH, ORIGINAL_PATH, true);
+        when(bundleContext.registerService(anyString(), eq(underTestOverlay), any(Dictionary.class))).thenReturn(serviceRegistrationOverlay);
+        underTestOverlay.registerService(bundleContext);
+
+        // prepare test resources
+        prepareOriginalResource(originalRootResource, ORIGINAL_PATH);
+        prepareOriginalResource(originalSubResource, ORIGINAL_PATH + "/sub1");
+        when(resourceResolver.listChildren(originalRootResource)).thenAnswer(new Answer<Iterator<Resource>>() {
+            public Iterator<Resource> answer(InvocationOnMock invocation) {
+                return Arrays.asList(new Resource[] { originalSubResource }).iterator();
+            }
+        });
+    }
+
+    private void prepareOriginalResource(Resource mockResource, String path) {
+        // prepare resource
+        when(mockResource.getPath()).thenReturn(path);
+        when(mockResource.getResourceType()).thenReturn(RESOURCE_TYPE);
+        when(mockResource.getResourceSuperType()).thenReturn(null);
+        ResourceMetadata resourceMetadata = new ResourceMetadata();
+        resourceMetadata.setResolutionPath(path);
+        when(mockResource.getResourceMetadata()).thenReturn(resourceMetadata);
+        when(mockResource.getResourceResolver()).thenReturn(resourceResolver);
+
+        // mount in resource tree
+        when(resourceResolver.getResource(path)).thenReturn(mockResource);
+    }
+
+    @After
+    public void tearDown() {
+        underTest.unregisterService();
+        verify(serviceRegistration).unregister();
+
+        underTestOverlay.unregisterService();
+        verify(serviceRegistrationOverlay).unregister();
+    }
+
+    @Test
+    public void testGetter() {
+        assertEquals(SUPERIMPOSED_PATH, underTest.getRootPath());
+        assertEquals(ORIGINAL_PATH, underTest.getSourcePath());
+        assertFalse(underTest.isOverlayable());
+    }
+
+    @Test
+    public void testGetterOverlay() {
+        assertEquals(SUPERIMPOSED_PATH, underTestOverlay.getRootPath());
+        assertEquals(ORIGINAL_PATH, underTestOverlay.getSourcePath());
+        assertTrue(underTestOverlay.isOverlayable());
+    }
+
+    @Test
+    public void testEquals() {
+        assertTrue(underTest.equals(underTest));
+        assertFalse(underTest.equals(underTestOverlay));
+        assertTrue(underTestOverlay.equals(underTestOverlay));
+        assertFalse(underTest.equals(underTestOverlay));
+    }
+
+    @Test
+    public void testGetMappedRootResource() {
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH);
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH, resource.getPath());
+
+        resource = underTestOverlay.getResource(resourceResolver, SUPERIMPOSED_PATH);
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH, resource.getPath());
+    }
+
+    @Test
+    public void testGetMappedRootResourceWithOverlay() throws RepositoryException {
+        when(resourceResolver.adaptTo(Session.class)).thenReturn(session);
+        when(session.itemExists(SUPERIMPOSED_PATH)).thenReturn(true);
+
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH);
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH, resource.getPath());
+
+        // root path cannot be overlayed
+        resource = underTestOverlay.getResource(resourceResolver, SUPERIMPOSED_PATH);
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH, resource.getPath());
+    }
+
+    @Test
+    public void testGetMappedSubResource() {
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub1");
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH + "/sub1", resource.getPath());
+
+        resource = underTestOverlay.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub1");
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH + "/sub1", resource.getPath());
+    }
+
+    @Test
+    public void testGetMappedSubResourceWithOverlay() throws RepositoryException {
+        when(resourceResolver.adaptTo(Session.class)).thenReturn(session);
+        when(session.itemExists(SUPERIMPOSED_PATH + "/sub1")).thenReturn(true);
+
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub1");
+        assertTrue(resource instanceof SuperimposingResource);
+        assertEquals(SUPERIMPOSED_PATH + "/sub1", resource.getPath());
+
+        // overlay item exists, allow underlying resource provider to step in
+        resource = underTestOverlay.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub1");
+        assertNull(resource);
+    }
+
+    @Test
+    public void testGetMappedNonExistingResource() {
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub2");
+        assertNull(resource);
+
+        resource = underTestOverlay.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub2");
+        assertNull(resource);
+    }
+
+    @Test
+    public void testGetMappedNonExistingResourceWithOverlay() throws RepositoryException {
+        when(resourceResolver.adaptTo(Session.class)).thenReturn(session);
+        when(session.itemExists(SUPERIMPOSED_PATH + "/sub2")).thenReturn(true);
+
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub2");
+        assertNull(resource);
+
+        resource = underTestOverlay.getResource(resourceResolver, SUPERIMPOSED_PATH + "/sub2");
+        assertNull(resource);
+    }
+
+    @Test
+    public void testGetMappedResourceRootInvalidPath() {
+        Resource resource = underTest.getResource(resourceResolver, "/invalid/path");
+        assertNull(resource);
+
+        resource = underTestOverlay.getResource(resourceResolver, "/invalid/path");
+        assertNull(resource);
+    }
+
+    @Test
+    public void testListChildren() {
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH);
+        Iterator<Resource> iterator = underTest.listChildren(resource);
+        assertTrue(iterator.hasNext());
+        assertEquals(SUPERIMPOSED_PATH + "/sub1", iterator.next().getPath());
+    }
+
+    @Test
+    public void testListChildrenWithResourceWrapper() {
+        Resource resource = underTest.getResource(resourceResolver, SUPERIMPOSED_PATH);
+        Iterator<Resource> iterator = underTest.listChildren(new ResourceWrapper(resource));
+        assertTrue(iterator.hasNext());
+        assertEquals(SUPERIMPOSED_PATH + "/sub1", iterator.next().getPath());
+    }
+
+    @Test
+    public void testListChildrenNonSuperimposingResource() {
+        Iterator<Resource> iterator = underTest.listChildren(mock(Resource.class));
+        assertNull(iterator);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceTest.java b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceTest.java
new file mode 100644
index 0000000..50233a6
--- /dev/null
+++ b/src/test/java/org/apache/sling/superimposing/impl/SuperimposingResourceTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.superimposing.impl;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import org.apache.sling.api.adapter.AdapterManager;
+import org.apache.sling.api.adapter.SlingAdaptable;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SuperimposingResourceTest {
+
+    private Resource underTest;
+
+    @Mock
+    private Resource originalResource;
+    @Mock
+    private ResourceResolver resourceResolver;
+    private ResourceMetadata resourceMetadata = new ResourceMetadata();
+
+    private static final String ORIGINAL_PATH = "/root/path1";
+    private static final String SUPERIMPOSED_PATH = "/root/path2";
+    private static final String RESOURCE_TYPE = "/resourceType1";
+
+    @Before
+    public void setUp() {
+        when(originalResource.getPath()).thenReturn(ORIGINAL_PATH);
+        when(originalResource.getResourceType()).thenReturn(RESOURCE_TYPE);
+        when(originalResource.getResourceSuperType()).thenReturn(null);
+        when(originalResource.getResourceMetadata()).thenReturn(this.resourceMetadata);
+        when(originalResource.getResourceResolver()).thenReturn(this.resourceResolver);
+        resourceMetadata.setResolutionPath(ORIGINAL_PATH);
+        underTest = new SuperimposingResource(this.originalResource, SUPERIMPOSED_PATH);
+    }
+
+    @Test
+    public void testGetter() {
+        assertEquals(SUPERIMPOSED_PATH, underTest.getPath());
+        assertEquals(RESOURCE_TYPE, underTest.getResourceType());
+        assertNull(underTest.getResourceSuperType());
+        assertEquals(ORIGINAL_PATH, underTest.getResourceMetadata().getResolutionPath());
+        assertSame(this.resourceResolver, underTest.getResourceResolver());
+    }
+
+    /**
+     * Make sure adaptions are inherited from source resource, but can be overridden by superimposing resource instance.
+     */
+    @Test
+    public void testAdaptTo() {
+        SlingAdaptable.setAdapterManager(new AdapterManager() {
+            @SuppressWarnings("unchecked")
+            public <AdapterType> AdapterType getAdapter(Object adaptable, Class<AdapterType> type) {
+                if (adaptable instanceof SuperimposingResource && type==String.class) {
+                    return (AdapterType)"mystring";
+                }
+                return null;
+            }
+        });
+        when(this.originalResource.adaptTo(String.class)).thenReturn("myoriginalstring");
+        when(this.originalResource.adaptTo(Integer.class)).thenReturn(12345);
+
+        assertEquals("mystring", underTest.adaptTo(String.class));
+        assertEquals((Integer)12345, underTest.adaptTo(Integer.class));
+        assertNull(underTest.adaptTo(Boolean.class));
+    }
+
+}

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