[maven-release-plugin] copy for tag org.apache.sling.serviceusermapper-1.2.4

git-svn-id: https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.serviceusermapper-1.2.4@1775083 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/serviceusermapper/pom.xml b/serviceusermapper/pom.xml
new file mode 100644
index 0000000..3831fc8
--- /dev/null
+++ b/serviceusermapper/pom.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+    
+    http://www.apache.org/licenses/LICENSE-2.0
+    
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>26</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.serviceusermapper</artifactId>
+    <packaging>bundle</packaging>
+    <version>1.2.4</version>
+
+    <name>Apache Sling Service User Mapper</name>
+    <description>
+        Provides a service to map service names with
+        optional service information to user names to
+        be used to access repositories such as the JCR
+        repository or the Sling ResourceResolver.
+    </description>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.serviceusermapper-1.2.4</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.serviceusermapper-1.2.4</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.serviceusermapper-1.2.4</url>
+    </scm>
+
+    <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>
+                        <Import-Package>
+                            org.apache.sling.commons.json,
+                            org.apache.felix.inventory;resolution:=optional,
+                            *
+                        </Import-Package>
+                       <DynamicImport-Package>
+                            org.apache.felix.inventory
+                        </DynamicImport-Package>
+                     </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.3.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.commons.json</artifactId>
+            <version>2.0.6</version>
+            <scope>provided</scope>
+        </dependency>
+       <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.inventory</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+  
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-api</artifactId>
+            <version>2.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>4.3.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>biz.aQute</groupId>
+            <artifactId>bndlib</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Testing -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- using log4j under slf4j to allow fine-grained logging config (see src/test/resources/log4j.properties) -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <version>1.5.0</version>            
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.13</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.9.5</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapped.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapped.java
new file mode 100644
index 0000000..949a1bf
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapped.java
@@ -0,0 +1,43 @@
+/*
+ * 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.serviceusermapping;
+
+import aQute.bnd.annotation.ProviderType;
+
+/**
+ * The <code>ServiceUserMapped</code> is a marker service that can be used to ensure that there is an already registered mapping for a certain service/subService.
+ * A service reference targeting a <code>ServiceUserMapped</code> will be satisfied only if <code>ServiceUserMapper.getServiceUserID</code>
+ * will resolve the subService to an userID.
+ * For example setting the reference target to "(subServiceName=mySubService)" ensures that your component only starts when the subService is available.
+ * The subServiceName will not be set for mappings that do not have one, and those can be referenced with a negating target "(!(subServiceName=*))".
+ * Trying to reference a sub service from a bundle for which it was not registered for will not work.
+ *
+ * As the service user mapper implementation is using a fallback, it is usually best to use a reference target that includes both
+ * options, the sub service name and the fallback, therefore a target like "(|((subServiceName=mySubService)(!(subServiceName=*))))" should be used.
+ */
+@ProviderType
+public interface ServiceUserMapped {
+
+
+    /**
+     * The name of the osgi property holding the sub service name.
+     */
+    static String SUBSERVICENAME = "subServiceName";
+
+}
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java
new file mode 100644
index 0000000..21e801d
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java
@@ -0,0 +1,75 @@
+/*
+ * 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.serviceusermapping;
+
+import org.osgi.framework.Bundle;
+
+import aQute.bnd.annotation.ProviderType;
+
+/**
+ * The <code>ServiceUserMapper</code> service can be used to map a service
+ * provided by a bundle to the ID of a user account used to access the
+ * ResourceResolver used by the service to access its data.
+ * <p>
+ * The goal of this service is to allow services to be implemented accessing the
+ * storage with service-specific accounts which are tailored to allow the
+ * service appropriate access without requiring administrative level access to
+ * the storage.
+ * <p>
+ * In general a service is implement in a single bundle such as the JSP compiler
+ * bundle. Other services may be implemented in multiple bundles. In certain
+ * cases there may be sub-services requiring different access levels. For
+ * example a couple of bundles may implement a "mail" service where each bundle
+ * implements a part of the service such as the "smtp", "queuing", and
+ * "delivery" sub services. Such sub services are identified with the
+ * {@code subServiceName} parameter on the method calls.
+ * <p>
+ * In addition to allowing to phase out the use of
+ * {@code ResourceResolver.getAdministrativeResourceResolver} and
+ * {@code SlingRepository.loginAdministrative} it also allows to better account
+ * for changes to the storage by the different services.
+ * <p>
+ * This service is not intended to be used by the general user but by
+ * implementations of the {@code ResourceResolverFactory} and
+ * {@code SlingRepository} services.
+ * <p>
+ * This service is not intended to be implemented by clients.
+ *
+ * @see <a href=
+ *      "http://sling.apache.org/documentation/the-sling-engine/service-authentication.html"
+ *      >Service Authentication</a>
+ */
+@ProviderType
+public interface ServiceUserMapper {
+
+    /**
+     * Returns the ID of a user to access the data store on behalf of the
+     * service.
+     *
+     * @param bundle The bundle implementing the service request access to
+     *            resources.
+     * @param subServiceName Name of the sub service. This parameter is optional and
+     *            may be an empty string or {@code null}.
+     * @return The ID of the user to use to provide access to the resources for
+     *         the service. This may be {@code null} if no particular user can
+     *         be derived for the service identified by the bundle and the
+     *         optional {@code serviceInfo}.
+     */
+    String getServiceUserID(Bundle bundle, String subServiceName);
+}
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserValidator.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserValidator.java
new file mode 100644
index 0000000..7930582
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/ServiceUserValidator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.serviceusermapping;
+
+import aQute.bnd.annotation.ConsumerType;
+
+/**
+ * The {@code ServiceUserValidator} allows to implement validation of configured
+ * service user mappings.
+ */
+@ConsumerType
+public interface ServiceUserValidator {
+
+    /**
+     * Validates the configured service user ID.
+     *
+     * @param serviceUserId The ID of the configured service user.
+     * @return {@code true} if the configured service user is valid; {@code false} otherwise.
+     */
+    boolean isValid(String serviceUserId, String serviceName, String subServiceName);
+}
\ No newline at end of file
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java
new file mode 100644
index 0000000..b4650c2
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java
@@ -0,0 +1,139 @@
+/*
+ * 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.serviceusermapping.impl;
+
+/**
+ * The <code>Mapping</code> class defines the mapping of a service's name and
+ * optional service information to a user name.
+ */
+class Mapping implements Comparable<Mapping> {
+
+
+    /**
+     * The name of the osgi property holding the service name.
+     */
+    static String SERVICENAME = ".serviceName";
+
+    private final String serviceName;
+
+    private final String subServiceName;
+
+    private final String userName;
+
+    /**
+     * Creates a mapping entry for the entry specification of the form:
+     *
+     * <pre>
+     * spec = serviceName [ ":" subServiceName ] "=" userName .
+     * </pre>
+     *
+     * @param spec The mapping specification.
+     * @throws NullPointerException if {@code spec} is {@code null}.
+     * @throws IllegalArgumentException if {@code spec} does not match the
+     *             expected pattern.
+     */
+    Mapping(final String spec) {
+
+        final int colon = spec.indexOf(':');
+        final int equals = spec.indexOf('=');
+
+        if (colon == 0 || equals <= 0) {
+            throw new IllegalArgumentException("serviceName is required");
+        } else if (equals == spec.length() - 1) {
+            throw new IllegalArgumentException("userName is required");
+        } else if (colon + 1 == equals) {
+            throw new IllegalArgumentException("serviceInfo must not be empty");
+        }
+
+        if (colon < 0 || colon > equals) {
+            this.serviceName = spec.substring(0, equals);
+            this.subServiceName = null;
+        } else {
+            this.serviceName = spec.substring(0, colon);
+            this.subServiceName = spec.substring(colon + 1, equals);
+        }
+
+        this.userName = spec.substring(equals + 1);
+    }
+
+    /**
+     * Returns the user name if the {@code serviceName} and the
+     * {@code serviceInfo} match. Otherwise {@code null} is returned.
+     *
+     * @param serviceName The name of the service to match. If this is
+     *            {@code null} this mapping will not match.
+     * @param subServiceName The Subservice Name to match. This may be
+     *            {@code null}.
+     * @return The user name if this mapping matches or {@code null} otherwise.
+     */
+    String map(final String serviceName, final String subServiceName) {
+        if (this.serviceName.equals(serviceName) && equals(this.subServiceName, subServiceName)) {
+            return userName;
+        }
+
+        return null;
+    }
+
+    private boolean equals(String str1, String str2) {
+        return ((str1 == null) ? str2 == null : str1.equals(str2));
+    }
+
+    @Override
+    public String toString() {
+        return "Mapping [serviceName=" + serviceName + ", subServiceName="
+                + subServiceName + ", userName=" + userName + "]";
+    }
+
+    public String getServiceName() {
+        return serviceName;
+    }
+
+    public String getSubServiceName() {
+        return subServiceName;
+    }
+
+
+    public int compareTo(Mapping o) {
+        if (o == null) {
+            return -1;
+        }
+
+        int result = compare(this.serviceName, o.serviceName);
+        if (result == 0) {
+            result = compare(this.subServiceName, o.subServiceName);
+        }
+        return result;
+    }
+
+    private int compare(String str1, String str2) {
+        if (str1 == str2) {
+            return 0;
+        }
+
+        if (str1 == null) {
+            return -1;
+        }
+
+        if (str2 == null) {
+            return 1;
+        }
+
+        return str1.hashCode() - str2.hashCode();
+    }
+}
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java
new file mode 100644
index 0000000..9b981e9
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java
@@ -0,0 +1,108 @@
+/*
+ * 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.serviceusermapping.impl;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Modified;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.PropertyUnbounded;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(metatype=true,
+        name="org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended",
+        label="Apache Sling Service User Mapper Service Amendment",
+        description="An amendment mapping for the user mapping service.",
+        configurationFactory=true,
+        policy=ConfigurationPolicy.REQUIRE)
+@Service(value={MappingConfigAmendment.class})
+@Properties({
+    @Property(name=Constants.SERVICE_RANKING, intValue=0, propertyPrivate=false,
+              label="Ranking",
+              description="Amendments are processed in order of their ranking, an amendment with a higher ranking has" +
+                          " precedence over a mapping with a lower ranking."),
+    @Property(name="webconsole.configurationFactory.nameHint", value="Mapping: {user.mapping}")
+})
+public class MappingConfigAmendment implements Comparable<MappingConfigAmendment> {
+
+    @Property(
+            label = "Service Mappings",
+            description = "Provides mappings from service name to user names. "
+                + "Each entry is of the form 'bundleId [ \":\" subServiceName ] \"=\" userName' "
+                + "where bundleId and subServiceName identify the service and userName "
+                + "defines the name of the user to provide to the service. Invalid entries are logged and ignored.",
+            unbounded = PropertyUnbounded.ARRAY)
+    private static final String PROP_SERVICE2USER_MAPPING = "user.mapping";
+
+    private static final String[] PROP_SERVICE2USER_MAPPING_DEFAULT = {};
+
+    /** default logger */
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private Mapping[] serviceUserMappings;
+
+    private int serviceRanking;
+
+    @Activate
+    @Modified
+    void configure(final Map<String, Object> config) {
+        final String[] props = PropertiesUtil.toStringArray(config.get(PROP_SERVICE2USER_MAPPING),
+            PROP_SERVICE2USER_MAPPING_DEFAULT);
+
+        final ArrayList<Mapping> mappings = new ArrayList<Mapping>(props.length);
+        for (final String prop : props) {
+            if (prop != null && prop.trim().length() > 0 ) {
+                try {
+                    final Mapping mapping = new Mapping(prop.trim());
+                    mappings.add(mapping);
+                } catch (final IllegalArgumentException iae) {
+                    logger.info("configure: Ignoring '{}': {}", prop, iae.getMessage());
+                }
+            }
+        }
+
+        this.serviceUserMappings = mappings.toArray(new Mapping[mappings.size()]);
+        this.serviceRanking = PropertiesUtil.toInteger(config.get(Constants.SERVICE_RANKING), 0);
+    }
+
+    public Mapping[] getServiceUserMappings() {
+        return this.serviceUserMappings;
+    }
+
+    public int compareTo(final MappingConfigAmendment o) {
+        // Sort by rank in descending order.
+        if ( this.serviceRanking > o.serviceRanking ) {
+            return -1; // lower rank
+        } else if (this.serviceRanking < o.serviceRanking) {
+            return 1; // higher rank
+        }
+
+        // If ranks are equal, then sort by hash code
+        return this.hashCode() < o.hashCode() ? -1 : 1;
+    }
+}
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java
new file mode 100644
index 0000000..b431b19
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java
@@ -0,0 +1,147 @@
+/*
+ * 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.serviceusermapping.impl;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.felix.inventory.Format;
+import org.apache.felix.inventory.InventoryPrinter;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Properties;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.json.JSONException;
+import org.apache.sling.commons.json.io.JSONWriter;
+
+/** InventoryPrinter for service user mappings */
+@Component
+@Service(value = InventoryPrinter.class)
+@Properties({
+    @Property(name = InventoryPrinter.FORMAT, value = { "JSON", "TEXT" }),
+    @Property(name = InventoryPrinter.NAME, value = "slingserviceusers"),
+    @Property(name = InventoryPrinter.TITLE, value = "Sling Service User Mappings"),
+    @Property(name = InventoryPrinter.WEBCONSOLE, boolValue = true)
+})
+public class MappingInventoryPrinter implements InventoryPrinter {
+
+    @Reference
+    private ServiceUserMapperImpl mapper;
+
+    @Override
+    public void print(PrintWriter out, Format format, boolean isZip) {
+        try {
+            if(format.equals(Format.JSON)) {
+                renderJson(out);
+            } else if(format.equals(Format.TEXT)) {
+                renderText(out);
+            }
+        } catch(Exception e) {
+            e.printStackTrace(out);
+        }
+    }
+
+    private String getMappedUser(Mapping m) {
+        return m.map(m.getServiceName(), m.getSubServiceName());
+    }
+
+    private SortedMap<String, List<Mapping>> getMappingsByUser(List<Mapping> mappings) {
+        SortedMap<String, List<Mapping>> result = new TreeMap<String, List<Mapping>>();
+        for(Mapping m : mappings) {
+            final String user = getMappedUser(m);
+            List<Mapping> list = result.get(user);
+            if(list == null) {
+                list = new ArrayList<Mapping>();
+                result.put(user, list);
+            }
+            list.add(m);
+        }
+        return result;
+    }
+
+    private void asJSON(JSONWriter w, Mapping m) throws JSONException {
+        w.object();
+        w.key("serviceName").value(m.getServiceName());
+        w.key("subServiceName").value(m.getSubServiceName());
+        w.key("user").value(getMappedUser(m));
+        w.endObject();
+    }
+
+    private void renderJson(PrintWriter out) throws JSONException {
+        final List<Mapping> data = mapper.getActiveMappings();
+        final Map<String, List<Mapping>> byUser = getMappingsByUser(data);
+
+        final JSONWriter w = new JSONWriter(out);
+        w.setTidy(true);
+        w.object();
+        w.key("title").value("Service User Mappings");
+        w.key("mappingsCount").value(data.size());
+        w.key("uniqueUsersCount").value(byUser.keySet().size());
+
+        w.key("mappingsByUser");
+        w.object();
+        for(Map.Entry<String, List<Mapping>> e : byUser.entrySet()) {
+            w.key(e.getKey());
+            w.array();
+            for(Mapping m : e.getValue()) {
+                asJSON(w,m);
+            }
+            w.endArray();
+        }
+        w.endObject();
+
+        w.endObject();
+    }
+
+    private void asText(PrintWriter w, Mapping m, String indent) {
+        final String SEP = " / ";
+        w.print(indent);
+        w.print(m.getServiceName());
+        w.print(SEP);
+        final String sub = m.getSubServiceName();
+        w.print(sub == null ? "" : sub);
+        w.print(SEP);
+        w.println(getMappedUser(m));
+    }
+
+    private void renderText(PrintWriter out) {
+        final List<Mapping> data = mapper.getActiveMappings();
+        final Map<String, List<Mapping>> byUser = getMappingsByUser(data);
+
+        final String formatInfo = " (format: service name / sub service name / user)";
+
+        out.print("*** Mappings by user (");
+        out.print(byUser.keySet().size());
+        out.print(" users):");
+        out.println(formatInfo);
+
+        for(Map.Entry<String, List<Mapping>> e : byUser.entrySet()) {
+            out.print("  ");
+            out.println(e.getKey());
+            for(Mapping m : e.getValue()) {
+                asText(out, m, "    ");
+            }
+        }
+   }
+}
\ No newline at end of file
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedBundleFilter.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedBundleFilter.java
new file mode 100644
index 0000000..c2179d2
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedBundleFilter.java
@@ -0,0 +1,94 @@
+/*
+ * 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.serviceusermapping.impl;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+@Component(immediate=true) // framework gets/ungets hooks each time
+@Service(value = {EventListenerHook.class, FindHook.class} )
+/**
+ * The <code>ServiceUserMappingBundleFilter</code> only allows the bundle for which the service mapping is available to see it.
+ */
+public class ServiceUserMappedBundleFilter implements EventListenerHook, FindHook {
+
+    @Override
+    public void event(ServiceEvent serviceEvent, Map map) {
+
+        ServiceReference serviceReference = serviceEvent.getServiceReference();
+        if (isServiceMappingReference(serviceReference)) {
+            Object serviceName = serviceReference.getProperty(Mapping.SERVICENAME);
+
+            if (serviceName != null && serviceName instanceof String) {
+                Iterator<Map.Entry<BundleContext, Collection<ListenerHook.ListenerInfo>>> it = map.entrySet().iterator();
+                while (it.hasNext()) {
+                    BundleContext ctx = it.next().getKey();
+
+                    String bundleServiceName = ServiceUserMapperImpl.getServiceName(ctx.getBundle());
+                    if (!serviceName.equals(bundleServiceName)) {
+                        it.remove();
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void find(BundleContext bundleContext, String name, String filter, boolean allServices,
+                     Collection references) {
+        String bundleServiceName = ServiceUserMapperImpl.getServiceName(bundleContext.getBundle());
+
+        Iterator<ServiceReference> it = references.iterator();
+        while (it.hasNext()) {
+            ServiceReference serviceReference = it.next();
+            if (isServiceMappingReference(serviceReference)) {
+                Object serviceName = serviceReference.getProperty(Mapping.SERVICENAME);
+
+                if (serviceName != null && !serviceName.equals(bundleServiceName)) {
+                    it.remove();
+                }
+            }
+        }
+    }
+
+    private static boolean isServiceMappingReference(ServiceReference serviceReference) {
+        Object objectClass = serviceReference.getProperty(Constants.OBJECTCLASS);
+        for (Object o :  (Object[]) objectClass) {
+            if (ServiceUserMappedImpl.SERVICEUSERMAPPED.equals(o)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+
+}
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedImpl.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedImpl.java
new file mode 100644
index 0000000..3a7d2d7
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.serviceusermapping.impl;
+
+import org.apache.sling.serviceusermapping.ServiceUserMapped;
+
+/**
+ * This is a trivial implementation of the marker interface <code>ServiceUserMapped</code>
+ */
+public class ServiceUserMappedImpl implements ServiceUserMapped {
+
+    static String SERVICEUSERMAPPED = ServiceUserMapped.class.getName();
+
+}
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java
new file mode 100644
index 0000000..b2f980e
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java
@@ -0,0 +1,440 @@
+/*
+ * 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.serviceusermapping.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+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.Modified;
+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.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.References;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.commons.osgi.PropertiesUtil;
+import org.apache.sling.serviceusermapping.ServiceUserMapped;
+import org.apache.sling.serviceusermapping.ServiceUserMapper;
+import org.apache.sling.serviceusermapping.ServiceUserValidator;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(
+        metatype = true,
+        label = "Apache Sling Service User Mapper Service",
+        description = "Configuration for the service mapping service names to names of users.")
+@Service(value={ServiceUserMapper.class, ServiceUserMapperImpl.class})
+@References( {
+    @Reference(name="amendment",
+            referenceInterface=MappingConfigAmendment.class,
+            cardinality=ReferenceCardinality.OPTIONAL_MULTIPLE,
+            policy=ReferencePolicy.DYNAMIC,
+            updated="updateAmendment"),
+    @Reference(name = "serviceUserValidator ", referenceInterface = ServiceUserValidator.class,
+    bind = "bindServiceUserValidator", unbind = "unbindServiceUserValidator",
+    cardinality= ReferenceCardinality.OPTIONAL_MULTIPLE, policy= ReferencePolicy.DYNAMIC)
+
+})
+
+public class ServiceUserMapperImpl implements ServiceUserMapper {
+
+    @Property(
+            label = "Service Mappings",
+            description = "Provides mappings from service name to user names. "
+                + "Each entry is of the form 'bundleId [ \":\" subServiceName ] \"=\" userName' "
+                + "where bundleId and subServiceName identify the service and userName "
+                + "defines the name of the user to provide to the service. Invalid entries are logged and ignored.",
+            unbounded = PropertyUnbounded.ARRAY)
+    private static final String PROP_SERVICE2USER_MAPPING = "user.mapping";
+
+    private static final String[] PROP_SERVICE2USER_MAPPING_DEFAULT = {};
+
+    private static final String PROP_DEFAULT_USER = "user.default";
+
+    @Property(
+            name = PROP_DEFAULT_USER,
+            label = "Default User",
+            description = "The name of the user to use as the default if no service mapping"
+                + "applies. If this property is missing or empty no default user is defined.")
+    private static final String PROP_DEFAULT_USER_DEFAULT = null;
+
+    /** default log */
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private Mapping[] globalServiceUserMappings = new Mapping[0];
+
+    private String defaultUser;
+
+    private Map<Long, MappingConfigAmendment> amendments = new HashMap<Long, MappingConfigAmendment>();
+
+    private Mapping[] activeMappings = new Mapping[0];
+
+    private final List<ServiceUserValidator> validators = new CopyOnWriteArrayList<ServiceUserValidator>();
+
+    private SortedMap<Mapping, Registration> activeRegistrations = new TreeMap<Mapping, Registration>();
+
+    private BundleContext bundleContext;
+
+    private ExecutorService executorService;
+
+    public boolean registerAsync = true;
+
+    @Activate
+    @Modified
+    synchronized void configure(BundleContext bundleContext, final Map<String, Object> config) {
+        if (registerAsync && executorService == null) {
+            executorService = Executors.newSingleThreadExecutor();
+        }
+
+        final String[] props = PropertiesUtil.toStringArray(config.get(PROP_SERVICE2USER_MAPPING),
+            PROP_SERVICE2USER_MAPPING_DEFAULT);
+
+        final ArrayList<Mapping> mappings = new ArrayList<Mapping>(props.length);
+        for (final String prop : props) {
+            if (prop != null && prop.trim().length() > 0 ) {
+                try {
+                    final Mapping mapping = new Mapping(prop.trim());
+                    mappings.add(mapping);
+                } catch (final IllegalArgumentException iae) {
+                    log.error("configure: Ignoring '{}': {}", prop, iae.getMessage());
+                }
+            }
+        }
+
+        this.globalServiceUserMappings = mappings.toArray(new Mapping[mappings.size()]);
+        this.defaultUser = PropertiesUtil.toString(config.get(PROP_DEFAULT_USER), PROP_DEFAULT_USER_DEFAULT);
+
+        RegistrationSet registrationSet = null;
+        this.bundleContext = bundleContext;
+        registrationSet = this.updateMappings();
+
+        this.executeServiceRegistrationsAsync(registrationSet);
+    }
+
+    @Deactivate
+    synchronized void deactivate() {
+        // this call does not unregister the mappings, but they should be unbound
+        // through the unbind methods anyway
+        updateServiceRegistrations(new Mapping[0]);
+        bundleContext = null;
+        if (executorService != null) {
+            executorService.shutdown();
+            executorService = null;
+        }
+    }
+
+    private void restartAllActiveServiceUserMappedServices() {
+        RegistrationSet registrationSet = new RegistrationSet();
+        registrationSet.removed = activeRegistrations.values();
+        registrationSet.added = activeRegistrations.values();
+        executeServiceRegistrationsAsync(registrationSet);
+    }
+
+    /**
+     * bind the serviceUserValidator
+     * @param serviceUserValidator
+     */
+    protected synchronized void bindServiceUserValidator(final ServiceUserValidator serviceUserValidator) {
+        validators.add(serviceUserValidator);
+        restartAllActiveServiceUserMappedServices();
+    }
+
+    /**
+     * unbind the serviceUserValidator
+     * @param serviceUserValidator
+     */
+    protected synchronized void unbindServiceUserValidator(final ServiceUserValidator serviceUserValidator) {
+        validators.remove(serviceUserValidator);
+        restartAllActiveServiceUserMappedServices();
+    }
+
+    /**
+     * @see org.apache.sling.serviceusermapping.ServiceUserMapper#getServiceUserID(org.osgi.framework.Bundle, java.lang.String)
+     */
+    @Override
+    public String getServiceUserID(final Bundle bundle, final String subServiceName) {
+        final String serviceName = getServiceName(bundle);
+        final String userId = internalGetUserId(serviceName, subServiceName);
+        final boolean valid = isValidUser(userId, serviceName, subServiceName);
+        final String result = valid ? userId : null;
+        log.debug(
+                "getServiceUserID(bundle {}, subServiceName {}) returns [{}] (raw userId={}, valid={})",
+                new Object[] { bundle, subServiceName, result, userId, valid });
+        return result;
+    }
+
+    protected synchronized void bindAmendment(final MappingConfigAmendment amendment, final Map<String, Object> props) {
+        final Long key = (Long) props.get(Constants.SERVICE_ID);
+        RegistrationSet registrationSet = null;
+        amendments.put(key, amendment);
+        registrationSet = this.updateMappings();
+        executeServiceRegistrationsAsync(registrationSet);
+    }
+
+    protected synchronized void unbindAmendment(final MappingConfigAmendment amendment, final Map<String, Object> props) {
+        final Long key = (Long) props.get(Constants.SERVICE_ID);
+        RegistrationSet registrationSet = null;
+        if ( amendments.remove(key) != null ) {
+             registrationSet = this.updateMappings();
+        }
+        executeServiceRegistrationsAsync(registrationSet);
+    }
+
+    protected void updateAmendment(final MappingConfigAmendment amendment, final Map<String, Object> props) {
+        this.bindAmendment(amendment, props);
+    }
+
+    protected RegistrationSet updateMappings() {
+        final List<MappingConfigAmendment> sortedMappings = new ArrayList<MappingConfigAmendment>();
+        for(final MappingConfigAmendment amendment : this.amendments.values() ) {
+            sortedMappings.add(amendment);
+        }
+        Collections.sort(sortedMappings);
+
+        final List<Mapping> mappings = new ArrayList<Mapping>();
+        for(final Mapping m : this.globalServiceUserMappings) {
+            mappings.add(m);
+        }
+        for(final MappingConfigAmendment mca : sortedMappings) {
+            for(final Mapping m : mca.getServiceUserMappings()) {
+                mappings.add(m);
+            }
+        }
+
+        activeMappings = mappings.toArray(new Mapping[mappings.size()]);
+        log.debug("Active mappings updated: {} mappings active", mappings.size());
+
+        RegistrationSet registrationSet = updateServiceRegistrations(activeMappings);
+
+        return registrationSet;
+
+    }
+
+
+    RegistrationSet updateServiceRegistrations(final Mapping[] newMappings) {
+
+        RegistrationSet result = new RegistrationSet();
+        // do not do anything if not activated
+        if (bundleContext == null) {
+            return result;
+        }
+
+        final SortedSet<Mapping> orderedNewMappings = new TreeSet<Mapping>(Arrays.asList(newMappings));
+        final SortedMap<Mapping, Registration> newRegistrations = new TreeMap<Mapping, Registration>();
+
+        // keep those that are still mapped
+        for (Map.Entry<Mapping, Registration> registrationEntry: activeRegistrations.entrySet()) {
+            boolean keepEntry = true;
+
+            if (!orderedNewMappings.contains(registrationEntry.getKey())) {
+                Registration registration = registrationEntry.getValue();
+
+                result.removed.add(registration);
+                keepEntry = false;
+            }
+
+            if (keepEntry) {
+                newRegistrations.put(registrationEntry.getKey(), registrationEntry.getValue());
+            }
+        }
+
+        // add those that are new
+        for (final Mapping mapping: orderedNewMappings) {
+            if (!newRegistrations.containsKey(mapping)) {
+                Registration registration = new Registration(mapping);
+                newRegistrations.put(mapping, registration);
+                result.added.add(registration);
+            }
+        }
+
+        activeRegistrations = newRegistrations;
+
+        return result;
+    }
+
+    private void executeServiceRegistrationsAsync(final RegistrationSet registrationSet) {
+
+        if (executorService == null) {
+            executeServiceRegistrations(registrationSet);
+        } else {
+            executorService.submit(new Runnable() {
+                @Override
+                public void run() {
+                    executeServiceRegistrations(registrationSet);
+                }
+            });
+        }
+    }
+
+
+    private void executeServiceRegistrations(final RegistrationSet registrationSet) {
+
+        if (registrationSet == null) {
+            return;
+        }
+
+        for (final Registration registration : registrationSet.removed) {
+
+
+            ServiceRegistration serviceRegistration = registration.setService(null);
+
+            if (serviceRegistration != null) {
+                try {
+                    serviceRegistration.unregister();
+                    log.debug("Unregistered ServiceUserMapped {}", registration.mapping);
+                } catch (final IllegalStateException e) {
+                    // this can happen on shutdown, therefore we just ignore it and don't log
+                }
+            }
+        }
+
+        BundleContext savedBundleContext = bundleContext;
+
+        if (savedBundleContext == null) {
+            return;
+        }
+
+        for (final Registration registration : registrationSet.added) {
+            Mapping mapping = registration.mapping;
+            final Dictionary<String, Object> properties = new Hashtable<String, Object>();
+            if (mapping.getSubServiceName() != null) {
+                properties.put(ServiceUserMapped.SUBSERVICENAME, mapping.getSubServiceName());
+            }
+
+            properties.put(Mapping.SERVICENAME, mapping.getServiceName());
+            final ServiceRegistration serviceRegistration = savedBundleContext.registerService(ServiceUserMappedImpl.SERVICEUSERMAPPED,
+                    new ServiceUserMappedImpl(), properties);
+
+            ServiceRegistration oldServiceRegistration = registration.setService(serviceRegistration);
+            log.debug("Activated ServiceUserMapped {}", registration.mapping);
+
+            if (oldServiceRegistration != null) {
+                try {
+                    oldServiceRegistration.unregister();
+                } catch (final IllegalStateException e) {
+                    // this can happen on shutdown, therefore we just ignore it and don't log
+                }
+            }
+        }
+
+    }
+
+    private String internalGetUserId(final String serviceName, final String subServiceName) {
+        log.debug(
+                "internalGetUserId: {} active mappings, looking for mapping for {}/{}",
+                new Object[] { this.activeMappings.length, serviceName, subServiceName });
+
+        for (final Mapping mapping : this.activeMappings) {
+            final String userId = mapping.map(serviceName, subServiceName);
+            if (userId != null) {
+                log.debug("Got userId [{}] from {}/{}", new Object[] { userId, serviceName, subServiceName });
+                return userId;
+            }
+        }
+
+        // second round without serviceInfo
+        log.debug(
+                "internalGetUserId: {} active mappings, looking for mapping for {}/<no subServiceName>",
+                this.activeMappings.length, serviceName);
+
+        for (Mapping mapping : this.activeMappings) {
+            final String userId = mapping.map(serviceName, null);
+            if (userId != null) {
+                log.debug("Got userId [{}] from {}/<no subServiceName>", userId, serviceName);
+                return userId;
+            }
+        }
+
+        log.debug("internalGetUserId: no mapping found, fallback to default user [{}]", this.defaultUser);
+        return this.defaultUser;
+    }
+
+    private boolean isValidUser(final String userId, final String serviceName, final String subServiceName) {
+        if (userId == null) {
+            log.debug("isValidUser: userId is null -> invalid");
+            return false;
+        }
+        if ( !validators.isEmpty() ) {
+            for (final ServiceUserValidator validator : validators) {
+                if ( validator.isValid(userId, serviceName, subServiceName) ) {
+                    log.debug("isValidUser: Validator {} accepts userId [{}] -> valid", validator, userId);
+                    return true;
+                }
+            }
+            log.debug("isValidUser: No validator accepted userId [{}] -> invalid", userId);
+            return false;
+        } else {
+            log.debug("isValidUser: No active validators for userId [{}] -> valid", userId);
+            return true;
+        }
+    }
+
+    static String getServiceName(final Bundle bundle) {
+        return bundle.getSymbolicName();
+    }
+
+    List<Mapping> getActiveMappings() {
+        return Collections.unmodifiableList(Arrays.asList(activeMappings));
+    }
+
+    class Registration {
+        private Mapping mapping;
+        private ServiceRegistration serviceRegistration;
+
+
+        Registration(Mapping mapping) {
+            this.mapping = mapping;
+            this.serviceRegistration = null;
+        }
+
+        synchronized ServiceRegistration setService(ServiceRegistration serviceRegistration) {
+            ServiceRegistration oldServiceRegistration = this.serviceRegistration;
+            this.serviceRegistration = serviceRegistration;
+            return oldServiceRegistration;
+        }
+    }
+
+    class RegistrationSet {
+        Collection<Registration> added = new ArrayList<Registration>();
+        Collection<Registration> removed = new ArrayList<Registration>();
+    }
+}
+
diff --git a/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/package-info.java b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/package-info.java
new file mode 100644
index 0000000..8eadc1a
--- /dev/null
+++ b/serviceusermapper/src/main/java/org/apache/sling/serviceusermapping/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+@Version("1.2")
+@Export(optional = "provide:=true")
+package org.apache.sling.serviceusermapping;
+
+import aQute.bnd.annotation.Export;
+import aQute.bnd.annotation.Version;
+
diff --git a/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java b/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java
new file mode 100644
index 0000000..7764d79
--- /dev/null
+++ b/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.serviceusermapping.impl;
+
+import java.lang.reflect.Field;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.serviceusermapping.impl.Mapping;
+import org.junit.Test;
+
+public class MappingTest {
+
+    @Test
+    public void test_constructor_null() {
+        try {
+            new Mapping(null);
+            TestCase.fail("NullPointerException expected");
+        } catch (NullPointerException npe) {
+            // expected
+        }
+    }
+
+    @Test
+    public void test_constructor_empty() {
+        try {
+            new Mapping("");
+            TestCase.fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    @Test
+    public void test_constructor_missing_user_name() {
+        try {
+            new Mapping("serviceName");
+            TestCase.fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+
+        try {
+            new Mapping("serviceName=");
+            TestCase.fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    @Test
+    public void test_constructor_missing_service_name() {
+        try {
+            new Mapping("=user");
+            TestCase.fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    @Test
+    public void test_constructor_empty_service_info() {
+        try {
+            new Mapping("srv:=user");
+            TestCase.fail("IllegalArgumentException expected");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+
+    @Test
+    public void test_constructor_user_with_colon() {
+        new Mapping("srv=jcr:user");
+    }
+
+    @Test
+    public void test_constructor_and_map() {
+        assertMapping("service", null, "user");
+        assertMapping("service", "subServiceName", "user");
+    }
+
+    private void assertMapping(final String serviceName, final String subServiceName, final String userName) {
+        StringBuilder spec = new StringBuilder();
+        spec.append(serviceName);
+        if (subServiceName != null) {
+            spec.append(':').append(subServiceName);
+        }
+        spec.append('=').append(userName);
+
+        // spec analysis
+        final Mapping mapping = new Mapping(spec.toString());
+        TestCase.assertEquals(getField(mapping, "serviceName"), serviceName);
+        TestCase.assertEquals(getField(mapping, "subServiceName"), subServiceName);
+        TestCase.assertEquals(getField(mapping, "userName"), userName);
+
+        // mapping
+        TestCase.assertEquals(userName, mapping.map(serviceName, subServiceName));
+        if (subServiceName == null) {
+            // Mapping without subServiceName must not match request with any
+            // subServiceName
+            TestCase.assertNull(mapping.map(serviceName, subServiceName + "-garbage"));
+        } else {
+            // Mapping with subServiceName must not match request without
+            // subServiceName
+            TestCase.assertNull(mapping.map(serviceName, null));
+        }
+
+        // no match for different service name
+        TestCase.assertNull(mapping.map(serviceName + "-garbage", subServiceName));
+
+        // no match for null service name
+        TestCase.assertNull(mapping.map(null, subServiceName));
+    }
+
+    private String getField(final Object object, final String fieldName) {
+        try {
+            Field f = object.getClass().getDeclaredField(fieldName);
+            f.setAccessible(true);
+            return (String) f.get(object);
+        } catch (Exception e) {
+            TestCase.fail("Cannot get field " + fieldName + ": " + e.toString());
+            return null; // will not get here, quiesce compiler
+        }
+    }
+}
diff --git a/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedBundleFilterTest.java b/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedBundleFilterTest.java
new file mode 100644
index 0000000..9f12426
--- /dev/null
+++ b/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMappedBundleFilterTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.serviceusermapping.impl;
+
+
+import junit.framework.TestCase;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test reference and bundle filtering based on <code>Mapping.SERVICENAME</code>
+ */
+public class ServiceUserMappedBundleFilterTest {
+
+    final static String BUNDLE1 = "bundle1";
+    final static String BUNDLE2 = "bundle2";
+
+
+    final static BundleContext bundleContext1;
+    final static BundleContext bundleContext2;
+
+    static  {
+        bundleContext1 = mock(BundleContext.class);
+        Bundle bundle1 = mock(Bundle.class);
+        when(bundleContext1.getBundle()).thenReturn(bundle1);
+        when(bundle1.getSymbolicName()).thenReturn(BUNDLE1);
+
+
+        bundleContext2 = mock(BundleContext.class);
+        Bundle bundle2 = mock(Bundle.class);
+        when(bundleContext2.getBundle()).thenReturn(bundle2);
+        when(bundle2.getSymbolicName()).thenReturn(BUNDLE2);
+
+    }
+
+
+
+
+    @Test
+    public void testEvent() {
+        Map<BundleContext, Collection<ListenerHook.ListenerInfo>> map = new HashMap<BundleContext, Collection<ListenerHook.ListenerInfo>>();
+
+        map.put(bundleContext1, new ArrayList<ListenerHook.ListenerInfo>());
+        map.put(bundleContext2, new ArrayList<ListenerHook.ListenerInfo>());
+
+        ServiceEvent serviceEvent = mock(ServiceEvent.class);
+        ServiceReference serviceReference = mock(ServiceReference.class);
+        when(serviceEvent.getServiceReference()).thenReturn(serviceReference);
+        when(serviceReference.getProperty(Constants.OBJECTCLASS)).thenReturn(new String[]{ServiceUserMappedImpl.SERVICEUSERMAPPED});
+        when(serviceReference.getProperty(Mapping.SERVICENAME)).thenReturn(BUNDLE1);
+
+
+        EventListenerHook eventListenerHook = new ServiceUserMappedBundleFilter();
+        eventListenerHook.event(serviceEvent, map);
+
+        TestCase.assertEquals(1, map.size());
+        TestCase.assertTrue(map.containsKey(bundleContext1));
+
+    }
+
+    @Test
+    public void testFind() {
+        List collection = new ArrayList<ServiceReference>();
+
+        ServiceReference serviceReference1 = mock(ServiceReference.class);
+        ServiceReference serviceReference2 = mock(ServiceReference.class);
+        collection.add(serviceReference1);
+        collection.add(serviceReference2);
+
+        when(serviceReference1.getProperty(Mapping.SERVICENAME)).thenReturn(BUNDLE1);
+        when(serviceReference1.getProperty(Constants.OBJECTCLASS)).thenReturn(new String[]{ServiceUserMappedImpl.SERVICEUSERMAPPED});
+
+        when(serviceReference2.getProperty(Mapping.SERVICENAME)).thenReturn(BUNDLE2);
+        when(serviceReference2.getProperty(Constants.OBJECTCLASS)).thenReturn(new String[]{ServiceUserMappedImpl.SERVICEUSERMAPPED});
+
+        FindHook findHook = new ServiceUserMappedBundleFilter();
+        findHook.find(bundleContext1, null, null, false, collection);
+
+        TestCase.assertEquals(1, collection.size());
+        TestCase.assertTrue(collection.contains(serviceReference1));
+    }
+}
diff --git a/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java b/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java
new file mode 100644
index 0000000..553f312
--- /dev/null
+++ b/serviceusermapper/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java
@@ -0,0 +1,328 @@
+/*
+ * 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.serviceusermapping.impl;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.serviceusermapping.ServiceUserValidator;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+public class ServiceUserMapperImplTest {
+    private static final String BUNDLE_SYMBOLIC1 = "bundle1";
+
+    private static final String BUNDLE_SYMBOLIC2 = "bundle2";
+
+    private static final String SUB = "sub";
+
+    private static final String NONE = "none";
+
+    private static final String SAMPLE = "sample";
+
+    private static final String ANOTHER = "another";
+
+    private static final String SAMPLE_SUB = "sample_sub";
+
+    private static final String ANOTHER_SUB = "another_sub";
+
+    private static final Bundle BUNDLE1;
+
+    private static final Bundle BUNDLE2;
+
+
+    static {
+        BUNDLE1 = mock(Bundle.class);
+        when(BUNDLE1.getSymbolicName()).thenReturn(BUNDLE_SYMBOLIC1);
+
+        BUNDLE2 = mock(Bundle.class);
+        when(BUNDLE2.getSymbolicName()).thenReturn(BUNDLE_SYMBOLIC2);
+    }
+
+
+
+
+    @Test
+    public void test_getServiceUserID() {
+        @SuppressWarnings("serial")
+        Map<String, Object> config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String[] {
+                    BUNDLE_SYMBOLIC1 + "=" + SAMPLE, //
+                    BUNDLE_SYMBOLIC2 + "=" + ANOTHER, //
+                    BUNDLE_SYMBOLIC1 + ":" + SUB + "=" + SAMPLE_SUB, //
+                    BUNDLE_SYMBOLIC2 + ":" + SUB + "=" + ANOTHER_SUB //
+                });
+                put("user.default", NONE);
+            }
+        };
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+
+        TestCase.assertEquals(SAMPLE, sum.getServiceUserID(BUNDLE1, null));
+        TestCase.assertEquals(ANOTHER, sum.getServiceUserID(BUNDLE2, null));
+        TestCase.assertEquals(SAMPLE, sum.getServiceUserID(BUNDLE1, ""));
+        TestCase.assertEquals(ANOTHER, sum.getServiceUserID(BUNDLE2, ""));
+        TestCase.assertEquals(SAMPLE_SUB, sum.getServiceUserID(BUNDLE1, SUB));
+        TestCase.assertEquals(ANOTHER_SUB, sum.getServiceUserID(BUNDLE2, SUB));
+    }
+
+    @Test
+    public void test_getServiceUserID_WithServiceUserValidator() {
+        @SuppressWarnings("serial")
+        Map<String, Object> config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String[] {
+                    BUNDLE_SYMBOLIC1 + "=" + SAMPLE, //
+                    BUNDLE_SYMBOLIC2 + "=" + ANOTHER, //
+                    BUNDLE_SYMBOLIC1 + ":" + SUB + "=" + SAMPLE_SUB, //
+                    BUNDLE_SYMBOLIC2 + ":" + SUB + "=" + ANOTHER_SUB //
+                });
+                put("user.default", NONE);
+            }
+        };
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+        ServiceUserValidator serviceUserValidator = new ServiceUserValidator() {
+
+            public boolean isValid(String serviceUserId, String serviceName,
+                    String subServiceName) {
+                if (SAMPLE.equals(serviceUserId)) {
+                    return false;
+                }
+                return true;
+            }
+        };
+        sum.bindServiceUserValidator(serviceUserValidator);
+
+        TestCase.assertEquals(null, sum.getServiceUserID(BUNDLE1, null));
+        TestCase.assertEquals(ANOTHER, sum.getServiceUserID(BUNDLE2, null));
+        TestCase.assertEquals(null, sum.getServiceUserID(BUNDLE1, ""));
+        TestCase.assertEquals(ANOTHER, sum.getServiceUserID(BUNDLE2, ""));
+        TestCase.assertEquals(SAMPLE_SUB, sum.getServiceUserID(BUNDLE1, SUB));
+        TestCase.assertEquals(ANOTHER_SUB, sum.getServiceUserID(BUNDLE2, SUB));
+    }
+
+    @Test
+    public void test_amendment() {
+        @SuppressWarnings("serial")
+        Map<String, Object> config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String[] {
+                    BUNDLE_SYMBOLIC1 + "=" + SAMPLE, //
+                    BUNDLE_SYMBOLIC1 + ":" + SUB + "=" + SAMPLE_SUB, //
+                });
+                put("user.default", NONE);
+            }
+        };
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+        final MappingConfigAmendment mca1 = new MappingConfigAmendment();
+        @SuppressWarnings("serial")
+        final Map<String, Object> mca1Config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String [] {BUNDLE_SYMBOLIC2 + "=" + ANOTHER});
+                put(Constants.SERVICE_ID, 1L);
+                put(Constants.SERVICE_RANKING, 100);
+            }
+        };
+        mca1.configure(mca1Config);
+        sum.bindAmendment(mca1, mca1Config);
+        final MappingConfigAmendment mca2 = new MappingConfigAmendment();
+        @SuppressWarnings("serial")
+        final Map<String, Object> mca2Config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String [] {BUNDLE_SYMBOLIC2 + ":" + SUB + "=" + ANOTHER_SUB});
+                put(Constants.SERVICE_ID, 2L);
+                put(Constants.SERVICE_RANKING, 200);
+            }
+        };
+        mca2.configure(mca2Config);
+        sum.bindAmendment(mca2, mca2Config);
+
+        TestCase.assertEquals(SAMPLE, sum.getServiceUserID(BUNDLE1, null));
+        TestCase.assertEquals(ANOTHER, sum.getServiceUserID(BUNDLE2, null));
+        TestCase.assertEquals(SAMPLE, sum.getServiceUserID(BUNDLE1, ""));
+        TestCase.assertEquals(ANOTHER, sum.getServiceUserID(BUNDLE2, ""));
+        TestCase.assertEquals(SAMPLE_SUB, sum.getServiceUserID(BUNDLE1, SUB));
+        TestCase.assertEquals(ANOTHER_SUB, sum.getServiceUserID(BUNDLE2, SUB));
+    }
+
+    @Test
+    public void test_amendmentOverlap() {
+        @SuppressWarnings("serial")
+        final Map<String, Object> config = new HashMap<String, Object>() {
+            {
+                put("user.default", NONE);
+            }
+        };
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+
+        final MappingConfigAmendment mca1 = new MappingConfigAmendment();
+        @SuppressWarnings("serial")
+        final Map<String, Object> mca1Config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String [] {BUNDLE_SYMBOLIC2 + "=" + ANOTHER});
+                put(Constants.SERVICE_RANKING, 100);
+            }
+        };
+        mca1.configure(mca1Config);
+        final MappingConfigAmendment mca2 = new MappingConfigAmendment();
+        @SuppressWarnings("serial")
+        final Map<String, Object> mca2Config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String [] {BUNDLE_SYMBOLIC2 + "=" + ANOTHER_SUB});
+                put(Constants.SERVICE_RANKING, 200);
+            }
+        };
+        mca2.configure(mca2Config);
+
+        sum.bindAmendment(mca1, mca1Config);
+        sum.bindAmendment(mca2, mca2Config);
+
+        TestCase.assertEquals(ANOTHER_SUB, sum.getServiceUserID(BUNDLE2, ""));
+    }
+
+
+
+    @Test
+    public void test_amendmentServiceUserMapping() {
+        @SuppressWarnings("serial")
+        Map<String, Object> config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String[] {
+                        BUNDLE_SYMBOLIC1 + "=" + SAMPLE, //
+                        BUNDLE_SYMBOLIC1 + ":" + SUB + "=" + SAMPLE_SUB, //
+                });
+                put("user.default", NONE);
+            }
+        };
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.registerAsync = false;
+        final ServiceRegistrationContextHelper context = new ServiceRegistrationContextHelper();
+        sum.configure(context.getBundleContext(), config);
+
+        TestCase.assertEquals(2, context.getRegistrations(ServiceUserMappedImpl.SERVICEUSERMAPPED).size());
+
+        final MappingConfigAmendment mca1 = new MappingConfigAmendment();
+        @SuppressWarnings("serial")
+        final Map<String, Object> mca1Config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String [] {BUNDLE_SYMBOLIC2 + "=" + ANOTHER});
+                put(Constants.SERVICE_ID, 1L);
+                put(Constants.SERVICE_RANKING, 100);
+            }
+        };
+        mca1.configure(mca1Config);
+        sum.bindAmendment(mca1, mca1Config);
+
+        TestCase.assertEquals(3, context.getRegistrations(ServiceUserMappedImpl.SERVICEUSERMAPPED).size());
+
+        final MappingConfigAmendment mca2 = new MappingConfigAmendment();
+        @SuppressWarnings("serial")
+        final Map<String, Object> mca2Config = new HashMap<String, Object>() {
+            {
+                put("user.mapping", new String [] {BUNDLE_SYMBOLIC2 + ":" + SUB + "=" + ANOTHER_SUB});
+                put(Constants.SERVICE_ID, 2L);
+                put(Constants.SERVICE_RANKING, 200);
+            }
+        };
+        mca2.configure(mca2Config);
+        sum.bindAmendment(mca2, mca2Config);
+
+        TestCase.assertEquals(4, context.getRegistrations(ServiceUserMappedImpl.SERVICEUSERMAPPED).size());
+
+        sum.unbindAmendment(mca1, mca1Config);
+
+        TestCase.assertEquals(3, context.getRegistrations(ServiceUserMappedImpl.SERVICEUSERMAPPED).size());
+    }
+
+
+    private class ServiceRegistrationContextHelper {
+
+
+        final BundleContext bundleContext = mock(BundleContext.class);
+        final Map<String, Map<Object, Dictionary>> registrations = new HashMap<String, Map<Object, Dictionary>>();
+
+        public ServiceRegistrationContextHelper() {
+            when(bundleContext.registerService(any(String.class), any(Object.class), any(Dictionary.class)))
+                    .then(new Answer<ServiceRegistration>() {
+                        public ServiceRegistration answer(InvocationOnMock invocationOnMock) throws Throwable {
+
+                            Object[] arguments = invocationOnMock.getArguments();
+                            return registerService((String) arguments[0], arguments[1], (Dictionary) arguments[2]);
+                        }
+                    });
+        }
+
+        private ServiceRegistration registerService(String string, Object o, Dictionary dictionary) {
+            if (!registrations.containsKey(string)) {
+                registrations.put(string, new HashMap<Object, Dictionary>());
+            }
+            final Map<Object, Dictionary> serviceRegistrations = registrations.get(string);
+            serviceRegistrations.put(o, dictionary);
+
+            final Object registeredObject = o;
+
+
+            return new ServiceRegistration() {
+                public ServiceReference getReference() {
+                    return null;
+                }
+
+                public void setProperties(Dictionary dictionary) {
+
+                }
+
+                public void unregister() {
+                    serviceRegistrations.remove(registeredObject);
+                }
+            };
+        }
+
+        public Map<Object, Dictionary> getRegistrations(String name) {
+            return registrations.get(name);
+        }
+
+        public BundleContext getBundleContext() {
+            return bundleContext;
+        }
+
+    }
+
+}