SLING-10902 ability to generate principalName from a hint (#7)

diff --git a/pom.xml b/pom.xml
index 5ea3306..b3e7515 100644
--- a/pom.xml
+++ b/pom.xml
@@ -122,7 +122,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.api</artifactId>
-            <version>2.23.7-SNAPSHOT</version>
+            <version>2.24.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateGroup.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateGroup.java
index 6899818..bf16611 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateGroup.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateGroup.java
@@ -42,7 +42,7 @@
      * Create a new group for the repository
      * 
      * @param jcrSession the JCR session of the user creating the group
-     * @param name The name of the new group (required)
+     * @param name The name of the new group.  If null or empty, the name is calculated from the supplied properties (per SLING-10902).
      * @param properties Extra properties to update on the group.  The entry values should be either a String or String[] (optional)
      * @param changes The list of changes for this operation (optional)
      * @return the group that was created
@@ -53,5 +53,22 @@
                             Map<String, ?> properties,
                             List<Modification> changes
                 ) throws RepositoryException;
-    
+
+    /**
+     * Create a new group for the repository.  The name is calculated from the 
+     * supplied properties (per SLING-10902).
+     * 
+     * @param jcrSession the JCR session of the user creating the group
+     * @param properties Extra properties to update on the group.  The entry values should be either a String or String[] (optional)
+     * @param changes The list of changes for this operation (optional)
+     * @return the group that was created
+     * @throws RepositoryException if group can't be created
+     */
+    public default Group createGroup(Session jcrSession,
+                            Map<String, ?> properties,
+                            List<Modification> changes
+                ) throws RepositoryException {
+        return createGroup(jcrSession, null, properties, changes);
+    }
+
 }
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateUser.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateUser.java
index 4da99fa..28e1118 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateUser.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/CreateUser.java
@@ -42,7 +42,7 @@
      * Create a new user for the repository
      * 
      * @param jcrSession the JCR session of the user creating the user
-     * @param name The name of the new user (required)
+     * @param name The name of the new user.  If null or empty, the name is calculated from the supplied properties (per SLING-10902).
      * @param password The password of the new user (required)
      * @param passwordConfirm The password of the new user again (required)
      * @param properties Extra properties to update on the user.  The entry values should be either a String or String[] (optional)
@@ -57,5 +57,25 @@
                             Map<String, ?> properties,
                             List<Modification> changes
                 ) throws RepositoryException;
-    
+
+    /**
+     * Create a new user for the repository. The name is calculated from the 
+     * supplied properties (per SLING-10902).
+     * 
+     * @param jcrSession the JCR session of the user creating the user
+     * @param password The password of the new user (required)
+     * @param passwordConfirm The password of the new user again (required)
+     * @param properties Extra properties to update on the user.  The entry values should be either a String or String[] (optional)
+     * @param changes The list of changes for this operation (optional)
+     * @return the user that was created
+     * @throws RepositoryException if user can't be created
+     */
+    public default User createUser(Session jcrSession,
+                            String password,
+                            String passwordConfirm,
+                            Map<String, ?> properties,
+                            List<Modification> changes
+                ) throws RepositoryException {
+        return createUser(jcrSession, null, password, passwordConfirm, properties, changes);
+    }
 }
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/PrincipalNameFilter.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/PrincipalNameFilter.java
new file mode 100644
index 0000000..6437f7d
--- /dev/null
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/PrincipalNameFilter.java
@@ -0,0 +1,32 @@
+/*
+ * 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.jackrabbit.usermanager;
+
+/**
+ * Service interface which allows for filtering what characters are allowed
+ * in a generated principal name
+ */
+public interface PrincipalNameFilter {
+
+    /**
+     * Filter the invalid characters out of the provided principal name candidate
+     * @param principalName the candidate principal name
+     * @return the filtered value
+     */
+    String filter(String principalName);
+
+}
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/PrincipalNameGenerator.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/PrincipalNameGenerator.java
new file mode 100644
index 0000000..6453d2b
--- /dev/null
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/PrincipalNameGenerator.java
@@ -0,0 +1,59 @@
+/*
+ * 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.jackrabbit.usermanager;
+
+import java.util.Map;
+
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+
+/**
+ * Service interface which allows for custom principal name generation
+ */
+public interface PrincipalNameGenerator {
+    public static class NameInfo {
+        private String principalName;
+        private boolean makeUnique = false;
+
+        public NameInfo(String principalName, boolean makeUnique) {
+            this.principalName = principalName;
+            this.makeUnique = makeUnique;
+        }
+
+        public String getPrincipalName() {
+            return principalName;
+        }
+
+        public boolean isMakeUnique() {
+            return makeUnique;
+        }
+    }
+
+    /**
+     * Get the to-be-created principal name candidate from the request.
+     *
+     * @param parameters the current request parameters map
+     * @param type the type of principal
+     * @param principalNameFilter for filtering what characters are allowed in a name 
+     * @param defaultPrincipalNameGenerator the default principal name generator
+     *
+     * @return the info about the principal name to be created or null if unable to do so
+     */
+    public NameInfo getPrincipalName(Map<String, ?> parameters, AuthorizableType type,
+            PrincipalNameFilter principalNameFilter,
+            PrincipalNameGenerator defaultPrincipalNameGenerator);
+
+}
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractAuthorizablePostServlet.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractAuthorizablePostServlet.java
index 9d1b7c9..d214143 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractAuthorizablePostServlet.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractAuthorizablePostServlet.java
@@ -19,10 +19,14 @@
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Array;
+import java.security.SecureRandom;
 import java.util.Calendar;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -33,10 +37,16 @@
 import javax.jcr.ValueFactory;
 
 import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.apache.sling.api.SlingIOException;
 import org.apache.sling.api.request.RequestParameter;
 import org.apache.sling.commons.osgi.OsgiUtil;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator.NameInfo;
 import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
+import org.apache.sling.jcr.base.util.AccessControlUtil;
 import org.apache.sling.servlets.post.Modification;
 import org.apache.sling.servlets.post.SlingPostConstants;
 import org.apache.sling.servlets.post.impl.helper.DateParser;
@@ -52,10 +62,29 @@
         AbstractPostServlet {
     private static final long serialVersionUID = -5918670409789895333L;
 
+    private static final class PrincipalNameGeneratorHolder {
+        private final PrincipalNameGenerator generator;
+        private final int ranking;
+
+        private PrincipalNameGeneratorHolder(PrincipalNameGenerator generator, int ranking) {
+            this.generator = generator;
+            this.ranking = ranking;
+        }
+
+        public PrincipalNameGenerator getGenerator() {
+            return generator;
+        }
+
+    }
+
+    protected static final String RP_NODE_NAME_VALUE_FROM = String.format("%s%s", SlingPostConstants.RP_NODE_NAME, SlingPostConstants.VALUE_FROM_SUFFIX);
+    protected static final String RP_NODE_NAME_HINT_VALUE_FROM = String.format("%s%s", SlingPostConstants.RP_NODE_NAME_HINT, SlingPostConstants.VALUE_FROM_SUFFIX);
+
     public static final String PROP_DATE_FORMAT = "servlet.post.dateFormats";
 
     private static final Logger LOG = LoggerFactory.getLogger(AbstractAuthorizablePostServlet.class);
 
+    private final SecureRandom randomCollisionIndex = new SecureRandom();
     private transient DateParser dateParser;
 
     protected transient SystemUserManagerPaths systemUserManagerPaths;
@@ -64,6 +93,112 @@
         this.systemUserManagerPaths = sump;
     }
 
+    /**
+     * The principal name generators
+     */
+    protected transient LinkedList<PrincipalNameGeneratorHolder> principalNameGenerators = new LinkedList<>();
+
+    /**
+     * The optional principal name filter
+     */
+    protected transient PrincipalNameFilter principalNameFilter;
+
+    /**
+     * Bind a new principal name generator
+     */
+//    @Reference(service = PrincipalNameGenerator.class)
+    protected void bindPrincipalNameGenerator(final PrincipalNameGenerator generator, final Map<String, Object> properties) {
+        final PrincipalNameGeneratorHolder pngh = new PrincipalNameGeneratorHolder(generator, getRanking(properties));
+        synchronized (principalNameGenerators) {
+            this.principalNameGenerators.add(pngh);
+            Collections.sort(this.principalNameGenerators, (o1, o2) -> 
+                Integer.compare(o1.ranking, o2.ranking));
+        }
+    }
+    protected void unbindPrincipalNameGenerator(final PrincipalNameGenerator generator) {
+        synchronized (principalNameGenerators) {
+            principalNameGenerators.removeIf(h -> h.generator == generator);
+        }
+    }
+
+    /**
+     * Bind a new principal name filter
+     */
+//    @Reference(service = PrincipalNameFilter.class)
+    protected void bindPrincipalNameFilter(final PrincipalNameFilter filter) {
+        this.principalNameFilter = filter;
+    }
+    protected void unbindPrincipalNameFilter(final PrincipalNameFilter filter) {
+        if (filter != null && filter.equals(this.principalNameFilter)) {
+            this.principalNameFilter = null;
+        }
+    }
+
+    /**
+     * Get or generate the name of the principal being created
+     * 
+     * @param request the current request
+     * @return the principal name
+     */
+    protected String getOrGeneratePrincipalName(Session jcrSession, Map<String, ?> properties, AuthorizableType type) throws RepositoryException {
+        String principalName = null;
+        PrincipalNameGenerator defaultPrincipalNameGenerator = null;
+        PrincipalNameGenerator principalNameGenerator = null;
+        synchronized (principalNameGenerators) {
+            if (!principalNameGenerators.isEmpty()) {
+                defaultPrincipalNameGenerator = principalNameGenerators.getFirst().getGenerator();
+                principalNameGenerator = principalNameGenerators.getLast().getGenerator();
+            }
+        }
+        if (principalNameGenerator != null) {
+            NameInfo nameInfo = principalNameGenerator.getPrincipalName(properties, type, 
+                    principalNameFilter, defaultPrincipalNameGenerator);
+            if (nameInfo == null && defaultPrincipalNameGenerator != null) {
+                // fallback to the default impl
+                nameInfo = defaultPrincipalNameGenerator.getPrincipalName(properties, type, 
+                        principalNameFilter, defaultPrincipalNameGenerator);
+            }
+            if (nameInfo != null) {
+                principalName = nameInfo.getPrincipalName();
+                if (principalName != null && nameInfo.isMakeUnique()) {
+                    // make sure the name is not already used
+                    UserManager um = AccessControlUtil.getUserManager(jcrSession);
+
+                    // if resulting authorizable exists, add a random suffix until it's not the case
+                    // anymore
+                    final int MAX_TRIES = 1000;
+                    if (um.getAuthorizable(principalName) != null ) {
+                        for (int i=0; i < MAX_TRIES; i++) {
+                            final int uniqueIndex = randomCollisionIndex.nextInt(9999);
+                            String newPrincipalName = principalName + "_" + uniqueIndex;
+                            if (um.getAuthorizable(newPrincipalName) == null) {
+                                // found unused value, so use it
+                                principalName = newPrincipalName;
+                                break;
+                            }
+                        }
+
+                        // Give up after MAX_TRIES
+                        if (um.getAuthorizable(principalName) != null ) {
+                            throw new RepositoryException(
+                                "Collision in generated principal names, generated name " + principalName + " already exists");
+                        }
+                    }
+                }
+            }
+        } else {
+            // fallback to the old behavior
+            Object obj = properties.get(SlingPostConstants.RP_NODE_NAME);
+            if (obj instanceof String[] && Array.getLength(obj) == 1) {
+                principalName = ((String[])obj)[0];
+            } else if (obj instanceof String) {
+                principalName= ((String)obj);
+            }
+        }
+
+        return principalName;
+    }
+
     // ---------- SCR Integration ----------------------------------------------
 
     protected void activate(Map<String, Object> props) {
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractPostServlet.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractPostServlet.java
index 542baa4..baac95f 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractPostServlet.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/AbstractPostServlet.java
@@ -459,7 +459,7 @@
         this.cachedPostResponseCreators = localCache;
     }
     
-    private int getRanking(final Map<String, Object> properties) {
+    protected int getRanking(final Map<String, Object> properties) {
         final Object val = properties.get(Constants.SERVICE_RANKING);
         return val instanceof Integer ? (Integer)val : 0;
     }
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateGroupServlet.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateGroupServlet.java
index 71ec9d4..f8e966e 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateGroupServlet.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateGroupServlet.java
@@ -28,12 +28,15 @@
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.apache.sling.api.SlingHttpServletRequest;
 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.jackrabbit.usermanager.CreateGroup;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
 import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
 import org.apache.sling.jcr.base.util.AccessControlUtil;
 import org.apache.sling.servlets.post.Modification;
@@ -47,6 +50,7 @@
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.component.annotations.ReferenceCardinality;
 import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 
 /**
  * <p>
@@ -65,8 +69,16 @@
  * </ul>
  * <h3>Post Parameters</h3>
  * <dl>
- * <dt>:name</dt>
- * <dd>The name of the new group (required)</dd>
+ * <dt>one of these</dt>
+ * <dd>
+ *   <ul>
+ *     <li><b>:name</b> - The value is the exact name to use</li>
+ *     <li><b>:name@ValueFrom</b> - The value is the name of another submitted parameter whose value is the exact name to use</li>
+ *     <li><b>:nameHint</b> - The value is filtered, trimmed and made unique</li>
+ *     <li><b>:nameHint@ValueFrom</b> - The value is the name of another submitted parameter whose value is filtered, trimmed and made unique</li>
+ *     <li><b>otherwise</b> - Try the value of any server-side configured "principalNameHints" parameter to treat as a hint that is filtered, trimmed and made unique</li>
+ *   </ul>
+ * </dd>
  * <dt>*</dt>
  * <dd>Any additional parameters become properties of the group node (optional)</dd>
  * </dl>
@@ -118,6 +130,31 @@
         super.deactivate();
     }
 
+    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
+            policy = ReferencePolicy.DYNAMIC)
+    @Override
+    protected void bindPrincipalNameGenerator(PrincipalNameGenerator generator, Map<String, Object> properties) {
+        super.bindPrincipalNameGenerator(generator, properties);
+    }
+
+    @Override
+    protected void unbindPrincipalNameGenerator(PrincipalNameGenerator generator) { // NOSONAR
+        super.unbindPrincipalNameGenerator(generator);
+    }
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL,
+            policy = ReferencePolicy.DYNAMIC,
+            policyOption = ReferencePolicyOption.GREEDY)
+    @Override
+    protected void bindPrincipalNameFilter(PrincipalNameFilter filter) {
+        super.bindPrincipalNameFilter(filter);
+    }
+
+    @Override
+    protected void unbindPrincipalNameFilter(PrincipalNameFilter filter) { // NOSONAR
+        super.unbindPrincipalNameFilter(filter);
+    }
+
     /* (non-Javadoc)
      * @see org.apache.sling.jackrabbit.usermanager.impl.post.AbstractAuthorizablePostServlet#bindSystemUserManagerPaths(org.apache.sling.jackrabbit.usermanager.impl.resource.SystemUserManagerPaths)
      */
@@ -187,21 +224,28 @@
             throw new IllegalArgumentException("JCR Session not found");
         }
 
-        if (name == null || name.length() == 0) {
+        final String principalName;
+        if (name == null || name.isEmpty()) {
+            principalName = getOrGeneratePrincipalName(jcrSession, properties, AuthorizableType.GROUP);
+        } else {
+            principalName = name;
+        }
+
+        if (principalName == null || principalName.length() == 0) {
             throw new IllegalArgumentException("Group name was not supplied");
         }
 
         UserManager userManager = AccessControlUtil.getUserManager(jcrSession);
-        Authorizable authorizable = userManager.getAuthorizable(name);
+        Authorizable authorizable = userManager.getAuthorizable(principalName);
 
         Group group = null;
         if (authorizable != null) {
             // principal already exists!
             throw new RepositoryException(
                 "A group already exists with the requested name: "
-                    + name);
+                    + principalName);
         } else {
-            group = userManager.createGroup(() -> name);
+            group = userManager.createGroup(() -> principalName);
 
             String groupPath = systemUserManagerPaths.getGroupPrefix()
                 + group.getID();
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateUserServlet.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateUserServlet.java
index 89262c9..a06f0ae 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateUserServlet.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/CreateUserServlet.java
@@ -30,10 +30,13 @@
 import org.apache.jackrabbit.api.security.user.User;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.jackrabbit.usermanager.CreateUser;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
 import org.apache.sling.jackrabbit.usermanager.resource.SystemUserManagerPaths;
 import org.apache.sling.jcr.api.SlingRepository;
 import org.apache.sling.jcr.base.util.AccessControlUtil;
@@ -50,6 +53,7 @@
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.component.annotations.ReferenceCardinality;
 import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.Designate;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
@@ -72,8 +76,16 @@
  * </ul>
  * <h3>Post Parameters</h3>
  * <dl>
- * <dt>:name</dt>
- * <dd>The name of the new user (required)</dd>
+ * <dt>one of these</dt>
+ * <dd>
+ *   <ul>
+ *     <li><b>:name</b> - The value is the exact name to use</li>
+ *     <li><b>:name@ValueFrom</b> - The value is the name of another submitted parameter whose value is the exact name to use</li>
+ *     <li><b>:nameHint</b> - The value is filtered, trimmed and made unique</li>
+ *     <li><b>:nameHint@ValueFrom</b> - The value is the name of another submitted parameter whose value is filtered, trimmed and made unique</li>
+ *     <li><b>otherwise</b> - Try the value of any server-side configured "principalNameHints" parameter to treat as a hint that is filtered, trimmed and made unique</li>
+ *   </ul>
+ * </dd>
  * <dt>:pwd</dt>
  * <dd>The password of the new user (required)</dd>
  * <dt>:pwdConfirm</dt>
@@ -182,9 +194,35 @@
     @Override
     @Deactivate
     protected void deactivate() {
+        this.selfRegistrationEnabled = false;
         super.deactivate();
     }
 
+    @Reference(cardinality = ReferenceCardinality.MULTIPLE,
+            policy = ReferencePolicy.DYNAMIC)
+    @Override
+    protected void bindPrincipalNameGenerator(PrincipalNameGenerator generator, Map<String, Object> properties) {
+        super.bindPrincipalNameGenerator(generator, properties);
+    }
+
+    @Override
+    protected void unbindPrincipalNameGenerator(PrincipalNameGenerator generator) { // NOSONAR
+        super.unbindPrincipalNameGenerator(generator);
+    }
+
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL,
+            policy = ReferencePolicy.DYNAMIC,
+            policyOption = ReferencePolicyOption.GREEDY)
+    @Override
+    protected void bindPrincipalNameFilter(PrincipalNameFilter filter) {
+        super.bindPrincipalNameFilter(filter);
+    }
+
+    @Override
+    protected void unbindPrincipalNameFilter(PrincipalNameFilter filter) { // NOSONAR
+        super.unbindPrincipalNameFilter(filter);
+    }
+
     /* (non-Javadoc)
      * @see org.apache.sling.jackrabbit.usermanager.impl.post.AbstractAuthorizablePostServlet#bindSystemUserManagerPaths(org.apache.sling.jackrabbit.usermanager.impl.resource.SystemUserManagerPaths)
      */
@@ -273,6 +311,13 @@
             throw new RepositoryException("JCR Session not found");
         }
 
+        final String principalName;
+        if (name == null || name.isEmpty()) {
+            principalName = getOrGeneratePrincipalName(jcrSession, properties, AuthorizableType.USER);
+        } else {
+            principalName = name;
+        }
+
         // check for an administrator
         boolean administrator = false;
         try {
@@ -305,7 +350,7 @@
 
 
         // check that the submitted parameter values have valid values.
-        if (name == null || name.length() == 0) {
+        if (principalName == null || principalName.length() == 0) {
             throw new RepositoryException("User name was not submitted");
         }
         if (password == null) {
@@ -328,15 +373,15 @@
             }
 
             UserManager userManager = AccessControlUtil.getUserManager(selfRegSession);
-            Authorizable authorizable = userManager.getAuthorizable(name);
+            Authorizable authorizable = userManager.getAuthorizable(principalName);
 
             if (authorizable != null) {
                 // user already exists!
                 throw new RepositoryException(
                     "A principal already exists with the requested name: "
-                        + name);
+                        + principalName);
             } else {
-                user = userManager.createUser(name, password);
+                user = userManager.createUser(principalName, password);
                 String userPath = systemUserManagerPaths.getUserPrefix()
                     + user.getID();
 
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/PrincipalNameGeneratorImpl.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/PrincipalNameGeneratorImpl.java
new file mode 100644
index 0000000..35ba3da
--- /dev/null
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/impl/post/PrincipalNameGeneratorImpl.java
@@ -0,0 +1,243 @@
+/*
+ * 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.jackrabbit.usermanager.impl.post;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Default implementation that generates a principal name based on a set of 
+ * well-known request parameters
+ * 
+ * <p>
+ * The value is resolved by the locating the first request parameter that is a 
+ * match of one of the choices in the following order:
+ * <ol>
+ * <li>":name" - value is the exact name to use</li>
+ * <li>":name@ValueFrom" - value is the name of another submitted parameter whose value is the exact name to use</li>
+ * <li>":nameHint" - value is filtered, trimmed and made unique</li>
+ * <li>":nameHint@ValueFrom" - value is the name of another submitted parameter whose value is filtered, trimmed and made unique</li>
+ * <li>otherwise, try the value of any configured "principalNameHints" parameters to treat as a hint that is filtered, trimmed and made unique</li>
+ * </ol>
+ * </p>
+ */
+@Component(
+        configurationPid = "org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator",
+        service = {PrincipalNameGenerator.class})
+@Designate(ocd = PrincipalNameGeneratorImpl.Config.class)
+public class PrincipalNameGeneratorImpl implements PrincipalNameGenerator {
+
+    @ObjectClassDefinition(name = "Apache Sling Principal Name Generator",
+            description = "The Sling helper to generate a principal name from a hint")
+    public @interface Config {
+
+        @AttributeDefinition(name = "Maximum Principal Name Length",
+                description = "Maximum number of characters to "+
+                 "use for automatically generated principal names. The default value is 20. Note, "+
+                 "that actual principal names may be generated with at most 4 more characters if "+
+                 "numeric suffixes must be appended to make the name unique.")
+        int principalNameMaxLength() default DEFAULT_MAX_NAME_LENGTH;
+
+        @AttributeDefinition(name = "Principal Name Hint Properties",
+                description = "The list of properties whose values "+
+                 "may be used to derive a name for newly created principal. When handling a request "+
+                 "to create a new principal, the name is automatically generated from this set if "+
+                 "no \":name\" or \":nameHint\" property is provided. In this case the request "+
+                 "parameters listed in this configuration value may be used as a hint to create the name.")
+        String[] principalNameHints();
+
+    }
+
+    private String[] parameterNames;
+
+    public static final int DEFAULT_MAX_NAME_LENGTH = 20;
+
+    private int maxLength = DEFAULT_MAX_NAME_LENGTH;
+
+    public PrincipalNameGeneratorImpl() {
+        this(null, -1);
+    }
+
+    public PrincipalNameGeneratorImpl(String[] parameterNames, int maxNameLength) {
+        if (parameterNames == null) {
+            this.parameterNames = new String[0];
+        } else {
+            this.parameterNames = parameterNames;
+        }
+
+        this.maxLength = (maxNameLength > 0)
+                ? maxNameLength
+                : DEFAULT_MAX_NAME_LENGTH;
+    }
+
+    @Activate
+    protected void activate(Config config) {
+        this.maxLength = config.principalNameMaxLength();
+        this.parameterNames = config.principalNameHints();
+    }
+
+    /**
+     * Convert the value to a list of strings
+     */
+    protected @NotNull List<String> valueToList(Object value) {
+        final List<String> valuesList;
+        if (value instanceof String[]) {
+            valuesList = Arrays.asList((String[])value);
+        } else if (value instanceof String) {
+            valuesList = Collections.singletonList((String)value);
+        } else if (value instanceof RequestParameter[]) {
+            valuesList = new ArrayList<>();
+            for (RequestParameter rp : (RequestParameter[])value) {
+                valuesList.add(rp.getString());
+            }
+        } else {
+            valuesList = Collections.emptyList();
+        }
+        return valuesList;
+    }
+
+    /**
+     * Determine the value to use for the specified parameter. This also
+     * considers the parameter with a {@link SlingPostConstants#VALUE_FROM_SUFFIX}
+     *
+     * @param parameters the map of request parameters
+     * @param paramName the parameter to get the value for
+     * @return the value to use for the parameter or null if it could not be determined
+     */
+    protected String getValueToUse(Map<String, ?> parameters, String paramName) {
+        String valueToUse = null;
+        List<String> values = valueToList(parameters.get(paramName));
+        if (!values.isEmpty()) {
+            for (String specialParam : values) {
+                if (specialParam != null && !specialParam.isEmpty()) {
+                    valueToUse = specialParam;
+                }
+
+                if (valueToUse != null) {
+                    if (valueToUse.isEmpty()) {
+                        // empty value is not usable
+                        valueToUse = null;
+                    } else {
+                        // found value, so stop looping
+                        break;
+                    }
+                }
+            }
+        } else {
+            // check for a paramName@ValueFrom param
+            // SLING-130: VALUE_FROM_SUFFIX means take the value of this
+            // property from a different field
+            values = valueToList(parameters.get(String.format("%s%s", paramName, SlingPostConstants.VALUE_FROM_SUFFIX)));
+            if (!values.isEmpty()) {
+                for (String specialParam : values) {
+                    if (specialParam != null && !specialParam.isEmpty()) {
+                        // retrieve the reference parameter value
+                        List<String> refValues = valueToList(parameters.get(specialParam));
+                        // @ValueFrom params must have exactly one value, else ignored
+                        if (refValues.size() == 1) {
+                            specialParam = refValues.get(0);
+                            if (specialParam != null && !specialParam.isEmpty()) {
+                                valueToUse = specialParam;
+                            }
+                        }
+                    }
+
+                    if (valueToUse != null) {
+                        if (valueToUse.isEmpty()) {
+                            // empty value is not usable
+                            valueToUse = null;
+                        } else {
+                            // found value, so stop looping
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        return valueToUse;
+    }
+
+    /**
+     * Get a "nice" principal name, if possible, based on given request
+     *
+     * @param request request
+     * @param type the type of principal
+     * @param defaultPrincipalNameGenerator the default principal name generator
+     *
+     * @return the principal name to be created or null if other PrincipalNameGenerators should be consulted
+     */
+    @Override
+    public NameInfo getPrincipalName(Map<String, ?> parameters, AuthorizableType type,
+            PrincipalNameFilter principalNameFilter, PrincipalNameGenerator defaultPrincipalNameGenerator) {
+        String valueToUse = null;
+        boolean doFilter = true;
+
+        // find the first request parameter that matches one of
+        // our parameterNames, in order, and has a value
+        // we first check for the special sling parameters
+        valueToUse = getValueToUse(parameters, SlingPostConstants.RP_NODE_NAME);
+        if (valueToUse != null) {
+            doFilter = false;
+        }
+        if ( valueToUse == null ) {
+            valueToUse = getValueToUse(parameters, SlingPostConstants.RP_NODE_NAME_HINT);
+
+            if (valueToUse == null && parameterNames != null) {
+                for (String param : parameterNames) {
+                    valueToUse = getValueToUse(parameters, param);
+                    if (valueToUse != null) {
+                        break;
+                    }
+                }
+            }
+        }
+
+        String result = valueToUse;
+        // should we filter?
+        if (doFilter && result != null && principalNameFilter != null) {
+            // filter value so that it works as a principal name
+            result = principalNameFilter.filter(result);
+        }
+
+        // max length
+        if (doFilter && result != null && result.length() > maxLength) {
+            result = result.substring(0, maxLength);
+        }
+
+        if (result != null) {
+            return new NameInfo(result, doFilter);
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jackrabbit/usermanager/package-info.java b/src/main/java/org/apache/sling/jackrabbit/usermanager/package-info.java
index a433e65..003bd56 100644
--- a/src/main/java/org/apache/sling/jackrabbit/usermanager/package-info.java
+++ b/src/main/java/org/apache/sling/jackrabbit/usermanager/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@org.osgi.annotation.versioning.Version("2.4.0")
+@org.osgi.annotation.versioning.Version("2.5.0")
 package org.apache.sling.jackrabbit.usermanager;
 
 
diff --git a/src/test/java/org/apache/sling/jackrabbit/usermanager/impl/post/PrincipalNameGeneratorImplTest.java b/src/test/java/org/apache/sling/jackrabbit/usermanager/impl/post/PrincipalNameGeneratorImplTest.java
new file mode 100644
index 0000000..267a282
--- /dev/null
+++ b/src/test/java/org/apache/sling/jackrabbit/usermanager/impl/post/PrincipalNameGeneratorImplTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.jackrabbit.usermanager.impl.post;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator.NameInfo;
+import org.apache.sling.jackrabbit.usermanager.impl.post.AbstractAuthorizablePostServlet.RequestParameterImpl;
+import org.apache.sling.jcr.jackrabbit.usermanager.it.post.CustomPrincipalNameFilterImpl;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class PrincipalNameGeneratorImplTest {
+
+    @Parameters(name = "Type: {0}")
+    public static Iterable<AuthorizableType> data() {
+        return Arrays.asList(AuthorizableType.USER, AuthorizableType.GROUP);
+    }
+
+    private PrincipalNameGenerator defaultGenerator = new PrincipalNameGeneratorImpl();
+    private AuthorizableType type;
+
+    public PrincipalNameGeneratorImplTest(AuthorizableType type) {
+        this.type = type;
+    }
+
+    @Test
+    public void testPrincipalNameFromName() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put(SlingPostConstants.RP_NODE_NAME, new RequestParameter[] {
+                new RequestParameterImpl("name1", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("name1", nameInfo.getPrincipalName());
+        assertFalse(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameHint() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put(SlingPostConstants.RP_NODE_NAME_HINT, new RequestParameter[] {
+                new RequestParameterImpl("name1", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("name1", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameHintWithFilter() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = new CustomPrincipalNameFilterImpl();
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put(SlingPostConstants.RP_NODE_NAME_HINT, new RequestParameter[] {
+                new RequestParameterImpl("Na me1", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("na_me1", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameHintTooLong() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put(SlingPostConstants.RP_NODE_NAME_HINT, new RequestParameter[] {
+                new RequestParameterImpl("namethatistoolong123456789", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("namethatistoolong123", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameValueFrom() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName", new RequestParameter[] {
+                new RequestParameterImpl("name1", "UTF-8")
+        });
+        parameters.put(String.format("%s%s", SlingPostConstants.RP_NODE_NAME, SlingPostConstants.VALUE_FROM_SUFFIX), new RequestParameter[] {
+                new RequestParameterImpl("displayName", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("name1", nameInfo.getPrincipalName());
+        assertFalse(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameValueFromTooLong() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName", new RequestParameter[] {
+                new RequestParameterImpl("namethatistoolong123456789", "UTF-8")
+        });
+        parameters.put(String.format("%s%s", SlingPostConstants.RP_NODE_NAME, SlingPostConstants.VALUE_FROM_SUFFIX), new RequestParameter[] {
+                new RequestParameterImpl("displayName", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("namethatistoolong123456789", nameInfo.getPrincipalName());
+        assertFalse(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameValueFromWithFilter() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = new CustomPrincipalNameFilterImpl();
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName", new RequestParameter[] {
+                new RequestParameterImpl("Na me1", "UTF-8")
+        });
+        parameters.put(String.format("%s%s", SlingPostConstants.RP_NODE_NAME, SlingPostConstants.VALUE_FROM_SUFFIX), new RequestParameter[] {
+                new RequestParameterImpl("displayName", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("Na me1", nameInfo.getPrincipalName());
+        assertFalse(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameHintValueFrom() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName", new RequestParameter[] {
+                new RequestParameterImpl("name1", "UTF-8")
+        });
+        parameters.put(String.format("%s%s", SlingPostConstants.RP_NODE_NAME_HINT, SlingPostConstants.VALUE_FROM_SUFFIX), new RequestParameter[] {
+                new RequestParameterImpl("displayName", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("name1", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameHintValueFromTooLong() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName", new RequestParameter[] {
+                new RequestParameterImpl("namethatistoolong123456789", "UTF-8")
+        });
+        parameters.put(String.format("%s%s", SlingPostConstants.RP_NODE_NAME_HINT, SlingPostConstants.VALUE_FROM_SUFFIX), new RequestParameter[] {
+                new RequestParameterImpl("displayName", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("namethatistoolong123", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromNameHintValueFromWithFilter() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = new CustomPrincipalNameFilterImpl();
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName", new RequestParameter[] {
+                new RequestParameterImpl("Na me1", "UTF-8")
+        });
+        parameters.put(String.format("%s%s", SlingPostConstants.RP_NODE_NAME_HINT, SlingPostConstants.VALUE_FROM_SUFFIX), new RequestParameter[] {
+                new RequestParameterImpl("displayName", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("na_me1", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromConfiguredHint() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl(new String[] {
+                    "displayName2"
+            },
+            10);
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName2", new RequestParameter[] {
+                new RequestParameterImpl("name1", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("name1", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromConfiguredHintWithFilter() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl(new String[] {
+                    "displayName2"
+            },
+            10);
+        PrincipalNameFilter filter = new CustomPrincipalNameFilterImpl();
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName2", new RequestParameter[] {
+                new RequestParameterImpl("Na me1", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("na_me1", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameFromConfiguredHintTooLong() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl(new String[] {
+                    "displayName2"
+            },
+            10);
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        parameters.put("displayName2", new RequestParameter[] {
+                new RequestParameterImpl("namethatistoolong", "UTF-8")
+        });
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNotNull(nameInfo);
+        assertEquals("namethatis", nameInfo.getPrincipalName());
+        assertTrue(nameInfo.isMakeUnique());
+    }
+
+    @Test
+    public void testPrincipalNameNotFound() {
+        PrincipalNameGenerator generator = new PrincipalNameGeneratorImpl();
+        PrincipalNameFilter filter = null;
+        Map<String, RequestParameter[]> parameters = new HashMap<>();
+        NameInfo nameInfo = generator.getPrincipalName(parameters, type, filter, defaultGenerator);
+        assertNull(nameInfo);
+    }
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/UserManagerTestSupport.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/UserManagerTestSupport.java
index b2996ad..4c58008 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/UserManagerTestSupport.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/UserManagerTestSupport.java
@@ -129,10 +129,10 @@
         //   may remove at a later date if the superclass includes these versions or later
         versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.api");
         versionResolver.setVersion("org.apache.sling", "org.apache.sling.engine", "2.7.10"); // to be compatible with current o.a.sling.api
-        versionResolver.setVersion("org.apache.sling", "org.apache.sling.resourceresolver", "1.7.10"); // to be compatible with current o.a.sling.api
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.resourceresolver", "1.8.0"); // to be compatible with current o.a.sling.api
         versionResolver.setVersion("org.apache.sling", "org.apache.sling.scripting.core", "2.4.0"); // to be compatible with current o.a.sling.api
         versionResolver.setVersion("org.apache.sling", "org.apache.sling.scripting.api", "2.2.0"); // to be compatible with current o.a.sling.api
-        versionResolver.setVersion("org.apache.sling", "org.apache.sling.servlets.resolver", "2.8.3-SNAPSHOT"); // to be compatible with current o.a.sling.api
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.servlets.resolver", "2.9.0"); // to be compatible with current o.a.sling.api
         versionResolver.setVersion("org.apache.sling", "org.apache.sling.commons.compiler", "2.4.0"); // to be compatible with current o.a.sling.scripting.core
 
         return composite(
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateGroupIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateGroupIT.java
index 3c06703..3f172ac 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateGroupIT.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateGroupIT.java
@@ -17,7 +17,9 @@
 package org.apache.sling.jcr.jackrabbit.usermanager.it.post;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -31,6 +33,7 @@
 import org.apache.http.auth.Credentials;
 import org.apache.http.auth.UsernamePasswordCredentials;
 import org.apache.http.message.BasicNameValuePair;
+import org.apache.sling.api.resource.ResourceUtil;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
@@ -207,4 +210,262 @@
         testCreateGroupRedirect("https://", SC_UNPROCESSABLE_ENTITY);
     }
 
+    /**
+     * SLING-10902 Test for group name that is not unique
+     */
+    @Test
+    public void testCreateGroupWithAlreadyUsedName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker, testGroupId);
+
+        // second time with the same info fails since it is not unique
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for group name that is not unique
+     */
+    @Test
+    public void testCreateGroupWithAlreadyUsedNameValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker, testGroupId);
+
+        // second time with the same info fails since it is not unique
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+    
+    /**
+     * SLING-10902 Test for group name generated from a hint
+     */
+    @Test
+    public void testCreateGroupWithNameHintAndAlreadyUsedName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String hint = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", hint));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(hint.substring(0, 20), testGroupId);
+
+        // second time with the same info generates a different unique name
+        json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId2  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId2);
+        assertTrue(testGroupId2.startsWith(hint.substring(0, 20)));
+        assertNotEquals(testGroupId, testGroupId2);
+    }
+
+
+    /**
+     * SLING-10902 Test for group name generated from the value of another param
+     */
+    @Test
+    public void testCreateGroupWithNameValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker, testGroupId);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated from a hint
+     */
+    @Test
+    public void testCreateGroupWithNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String hint = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", hint));
+        postParams.add(new BasicNameValuePair("marker", testUserId));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(hint.substring(0, 20), testGroupId);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated from a hint value of another param
+     */
+    @Test
+    public void testCreateGroupWithNameHintValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker.substring(0, 20), testGroupId);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated without a hint
+     */
+    @Test
+    public void testCreateGroupWithNoName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated without a hint
+     */
+    @Test
+    public void testCreateGroupWithEmptyName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", ""));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated without a hint
+     */
+    @Test
+    public void testCreateGroupWithEmptyNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", ""));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+
+    /**
+     * SLING-10902 Test for group name generated from a default property name
+     */
+    @Test
+    public void testCreateGroupWithNoNameAndAlternateHintProp() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker.substring(0, 20), testGroupId);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated from a default property name
+     */
+    @Test
+    public void testCreateGroupWithEmptyNameAndAlternateHintProp() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", ""));
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker.substring(0, 20), testGroupId);
+    }
+
+    /**
+     * SLING-10902 Test for group name generated from a default property name
+     */
+    @Test
+    public void testCreateGroupWithEmptyNameHintAndAlternateHintProp() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", ""));
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertEquals(marker.substring(0, 20), testGroupId);
+    }
+
 }
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreatePrincipalWithCustomNameGeneratorIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreatePrincipalWithCustomNameGeneratorIT.java
new file mode 100644
index 0000000..a5565b8
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreatePrincipalWithCustomNameGeneratorIT.java
@@ -0,0 +1,324 @@
+/*
+ * 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.jcr.jackrabbit.usermanager.it.post;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * Tests for the 'createUser' and 'createGroup' Sling Post Operation
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class CreatePrincipalWithCustomNameGeneratorIT extends UserManagerClientTestSupport {
+
+    protected ServiceRegistration<PrincipalNameGenerator> principalNameGeneratorServiceReg;
+    protected ServiceRegistration<PrincipalNameFilter> principalNameFilterServiceReg;
+
+    @Override
+    public void before() throws IOException, URISyntaxException {
+        Bundle bundle = FrameworkUtil.getBundle(getClass());
+        Dictionary<String, Object> props = new Hashtable<>(); // NOSONAR
+        props.put(Constants.SERVICE_RANKING, 1);
+        principalNameGeneratorServiceReg = bundle.getBundleContext().registerService(PrincipalNameGenerator.class,
+                new CustomPrincipalNameGeneratorImpl(), props);
+
+        Dictionary<String, Object> props2 = new Hashtable<>(); // NOSONAR
+        props2.put(Constants.SERVICE_RANKING, 1);
+        principalNameFilterServiceReg = bundle.getBundleContext().registerService(PrincipalNameFilter.class,
+                new CustomPrincipalNameFilterImpl(), props2);
+
+        super.before();
+    }
+
+    @Override
+    public void after() throws IOException {
+        if (principalNameGeneratorServiceReg != null) {
+            principalNameGeneratorServiceReg.unregister();
+            principalNameGeneratorServiceReg = null;
+        }
+        if (principalNameFilterServiceReg != null) {
+            principalNameFilterServiceReg.unregister();
+            principalNameFilterServiceReg = null;
+        }
+
+        super.after();
+    }
+
+    /**
+     * Test for user name generated from a hint
+     */
+    @Test
+    public void testCreateUserWithNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String hint = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", hint));
+        postParams.add(new BasicNameValuePair("marker", testUserId));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertTrue(testUserId.startsWith("custom_user_"));
+    }
+
+    /**
+     * Test for user name generated from a hint value of another param
+     */
+    @Test
+    public void testCreateUserWithNameHintValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertTrue(testUserId.startsWith("custom_user_"));
+    }
+
+    /**
+     * Test for user name generated without a hint
+     */
+    @Test
+    public void testCreateUserWithNoName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Test for user name generated without a hint
+     */
+    @Test
+    public void testCreateUserWithEmptyName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", ""));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Test for user name generated without a hint
+     */
+    @Test
+    public void testCreateUserWithEmptyNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", ""));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Test for user name generated from a default property name
+     */
+    @Test
+    public void testCreateUserWithHintFromDefaultPropertyName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertTrue(testUserId.startsWith("custom_user_"));
+    }
+
+
+
+    /**
+     * Test for group name generated from a hint
+     */
+    @Test
+    public void testCreateGroupWithNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String hint = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", hint));
+        postParams.add(new BasicNameValuePair("marker", testUserId));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertTrue(testGroupId.startsWith("custom_group_"));
+    }
+
+    /**
+     * Test for group name generated from a hint value of another param
+     */
+    @Test
+    public void testCreateGroupWithNameHintValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertTrue(testGroupId.startsWith("custom_group_"));
+    }
+
+    /**
+     * Test for group name generated without a hint
+     */
+    @Test
+    public void testCreateGroupWithNoName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Test for group name generated without a hint
+     */
+    @Test
+    public void testCreateGroupWithEmptyName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", ""));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Test for group name generated without a hint
+     */
+    @Test
+    public void testCreateGroupWithEmptyNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", ""));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * Test for group name generated from a default property name
+     */
+    @Test
+    public void testCreateGroupWithHintFromDefaultPropertyName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/group.create.json", baseServerUri);
+
+        String marker = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testGroupId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testGroupId);
+        assertTrue(testGroupId.startsWith("custom_group_"));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateUserIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateUserIT.java
index acf8e6e..a049ee3 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateUserIT.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CreateUserIT.java
@@ -18,7 +18,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -32,6 +34,7 @@
 import org.apache.http.auth.Credentials;
 import org.apache.http.auth.UsernamePasswordCredentials;
 import org.apache.http.message.BasicNameValuePair;
+import org.apache.sling.api.resource.ResourceUtil;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
@@ -303,4 +306,284 @@
         testCreateUserRedirect("https://", SC_UNPROCESSABLE_ENTITY);
     }
 
+    /**
+     * SLING-10902 Test for user name that is not unique
+     */
+    @Test
+    public void testCreateUserWithAlreadyUsedName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker, testUserId);
+
+        // second time with the same info fails since it is not unique
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for user name that is not unique
+     */
+    @Test
+    public void testCreateUserWithAlreadyUsedNameValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker, testUserId);
+
+        // second time with the same info fails since it is not unique
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated from a hint
+     */
+    @Test
+    public void testCreateUserWithNameHintAndAlreadyUsedName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String hint = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", hint));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(hint.substring(0, 20), testUserId);
+
+        // second time with the same info generates a different unique name
+        json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId2  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId2);
+        assertTrue(testUserId2.startsWith(hint.substring(0, 20)));
+        assertNotEquals(testUserId, testUserId2);
+    }
+
+
+    /**
+     * SLING-10902 Test for user name generated from the value of another param
+     */
+    @Test
+    public void testCreateUserWithNameValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker, testUserId);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated from a hint
+     */
+    @Test
+    public void testCreateUserWithNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String hint = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", hint));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(hint.substring(0, 20), testUserId);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated from a hint value of another param
+     */
+    @Test
+    public void testCreateUserWithNameHintValueFrom() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint@ValueFrom", "marker"));
+        postParams.add(new BasicNameValuePair("marker", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker.substring(0, 20), testUserId);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated without a hint but one of the alternate name hint
+     * properties is supplied
+     */
+    @Test
+    public void testCreateUserWithNoName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated without a hint but one of the alternate name hint
+     * properties is supplied
+     */
+    @Test
+    public void testCreateUserWithEmptyName() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", ""));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated without a hint but one of the alternate name hint
+     * properties is supplied
+     */
+    @Test
+    public void testCreateUserWithEmptyNameHint() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", ""));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated without a hint but one of the alternate name hint
+     * properties is supplied
+     */
+    @Test
+    public void testCreateUserWithNoNameAndAlternateHintProp() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker.substring(0, 20), testUserId);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated without a hint but one of the alternate name hint
+     * properties is supplied
+     */
+    @Test
+    public void testCreateUserWithEmptyNameAndAlternateHintProp() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", ""));
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker.substring(0, 20), testUserId);
+    }
+
+    /**
+     * SLING-10902 Test for user name generated without a hint but one of the alternate name hint
+     * properties is supplied
+     */
+    @Test
+    public void testCreateUserWithEmptyNameHintAndAlternateHintProp() throws IOException, JsonException {
+        String postUrl = String.format("%s/system/userManager/user.create.json", baseServerUri);
+
+        String marker = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":nameHint", ""));
+        postParams.add(new BasicNameValuePair("displayName", marker));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+        testUserId  = ResourceUtil.getName(jsonObj.getString("path"));
+        assertNotNull(testUserId);
+        assertEquals(marker.substring(0, 20), testUserId);
+    }
+
 }
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CustomPrincipalNameFilterImpl.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CustomPrincipalNameFilterImpl.java
new file mode 100644
index 0000000..61e0a0c
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CustomPrincipalNameFilterImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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.jcr.jackrabbit.usermanager.it.post;
+
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+
+/**
+ * Filter a String so that it can be used as a principal name.
+ */
+public class CustomPrincipalNameFilterImpl implements PrincipalNameFilter {
+
+    public static final String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_.-";
+    public static final char REPLACEMENT_CHAR = '_';
+
+    @Override
+    public String filter(String principalName) {
+        final StringBuilder sb  = new StringBuilder();
+        char lastAdded = 0;
+
+        principalName = principalName.toLowerCase();
+        for(int i=0; i < principalName.length(); i++) {
+            final char c = principalName.charAt(i);
+            char toAdd = c;
+
+            if (ALLOWED_CHARS.indexOf(c) < 0) {
+                if (lastAdded == REPLACEMENT_CHAR) {
+                    // do not add several _ in a row
+                    continue;
+                }
+                toAdd = REPLACEMENT_CHAR;
+
+            } else if(i == 0 && Character.isDigit(c)) {
+                sb.append(REPLACEMENT_CHAR);
+            }
+
+            sb.append(toAdd);
+            lastAdded = toAdd;
+        }
+
+        if (sb.length()==0) {
+            sb.append(REPLACEMENT_CHAR);
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CustomPrincipalNameGeneratorImpl.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CustomPrincipalNameGeneratorImpl.java
new file mode 100644
index 0000000..62a5f52
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/CustomPrincipalNameGeneratorImpl.java
@@ -0,0 +1,42 @@
+/*
+ * 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.jcr.jackrabbit.usermanager.it.post;
+
+import java.util.Map;
+
+import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameFilter;
+import org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator;
+
+/**
+ * Sample implementation of the PrincipalNameGenerator interface.
+ */
+public class CustomPrincipalNameGeneratorImpl implements PrincipalNameGenerator {
+
+    @Override
+    public NameInfo getPrincipalName(Map<String, ?> parameters, AuthorizableType type,
+            PrincipalNameFilter principalNameFilter, PrincipalNameGenerator defaultPrincipalNameGenerator) {
+        NameInfo nameInfo = defaultPrincipalNameGenerator.getPrincipalName(parameters, type, 
+                principalNameFilter, defaultPrincipalNameGenerator);
+        if (nameInfo != null && nameInfo.getPrincipalName() != null && nameInfo.isMakeUnique()) {
+            String principalName = String.format("custom_%s_%s", type.name().toLowerCase(), nameInfo.getPrincipalName());
+            nameInfo = new NameInfo(principalName, nameInfo.isMakeUnique());
+        }
+        return nameInfo;
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/UserManagerClientTestSupport.java b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/UserManagerClientTestSupport.java
index 64c96cc..48b23e3 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/UserManagerClientTestSupport.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/usermanager/it/post/UserManagerClientTestSupport.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.fail;
 import static org.ops4j.pax.exam.CoreOptions.when;
 import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
 
 import java.io.IOException;
 import java.io.StringReader;
@@ -118,6 +119,7 @@
     protected String testUserId2 = null;
     protected String testUserId3 = null;
     protected String testGroupId = null;
+    protected String testGroupId2 = null;
     protected String testFolderUrl = null;
 
     @Override
@@ -157,6 +159,10 @@
             factoryConfiguration("org.apache.sling.jcr.contentloader.hc.BundleContentLoadedCheck")
                 .put("hc.tags", new String[] {"bundles"})
                 .asOption(),
+            // SLING-10902 configure principal name hint
+            newConfiguration("org.apache.sling.jackrabbit.usermanager.PrincipalNameGenerator")
+                        .put("principalNameHints", new String[] {"displayName"})
+                        .asOption()
         };
     }
 
@@ -209,6 +215,11 @@
             String postUrl = String.format("%s/system/userManager/group/%s.delete.html", baseServerUri, testGroupId);
             assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, Collections.emptyList(), null);
         }
+        if (testGroupId2 != null) {
+            //remove the test user if it exists.
+            String postUrl = String.format("%s/system/userManager/group/%s.delete.html", baseServerUri, testGroupId2);
+            assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, Collections.emptyList(), null);
+        }
         if (testUserId != null) {
             //remove the test user if it exists.
             String postUrl = String.format("%s/system/userManager/user/%s.delete.html", baseServerUri, testUserId);