Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncRuntimeException.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncRuntimeException.java	(revision )
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncRuntimeException.java	(revision )
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.spi.security.authentication.external.impl.jmx;
+
+import javax.annotation.Nonnull;
+
+class SyncRuntimeException extends RuntimeException {
+
+    public SyncRuntimeException(@Nonnull String message) {
+        super(message);
+    }
+
+    public SyncRuntimeException(@Nonnull String message, @Nonnull Throwable throwable) {
+        super(message, throwable);
+    }
+}
\ No newline at end of file
Index: oak-auth-external/pom.xml
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/pom.xml	(revision 1708794)
+++ oak-auth-external/pom.xml	(revision )
@@ -154,6 +154,12 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>oak-jcr</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <scope>test</scope>
Index: oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImplTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImplTest.java	(revision )
+++ oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImplTest.java	(revision )
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.spi.security.authentication.external.impl.jmx;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.jcr.Repository;
+
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProviderManager;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.TestIdentityProvider;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncHandler;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class SyncMBeanImplTest {
+
+    private static final String SYNC_NAME = "testSyncName";
+
+    private static Repository REPOSITORY;
+
+    private ExternalIdentityProvider idp;
+    private SyncManager syncMgr;
+    ExternalIdentityProviderManager idpMgr;
+
+    private SyncMBeanImpl syncMBean;
+
+    @BeforeClass
+    public static void beforeClass() {
+        REPOSITORY = new Jcr().createRepository();
+    }
+
+    @Before
+    public void before() {
+        // TODO : property setup
+        idp = new TestIdentityProvider();
+        syncMgr = new SyncManager() {
+            @CheckForNull
+            @Override
+            public SyncHandler getSyncHandler(@Nonnull String name) {
+                if (SYNC_NAME.equals(name)) {
+                    return new DefaultSyncHandler(new DefaultSyncConfig());
+                } else {
+                    return null;
+                }
+            }
+        };
+        idpMgr = new ExternalIdentityProviderManager() {
+            @CheckForNull
+            @Override
+            public ExternalIdentityProvider getProvider(@Nonnull String name) {
+                if (name.equals(idp.getName())) {
+                    return idp;
+                } else {
+                    return null;
+                }
+            }
+        };
+        syncMBean = new SyncMBeanImpl(REPOSITORY, syncMgr, SYNC_NAME, idpMgr, idp.getName());
+    }
+
+    @Test
+    public void testGetSyncHandlerName() {
+        assertEquals(SYNC_NAME, syncMBean.getSyncHandlerName());
+    }
+
+    @Test
+    public void testInvalidSyncHandlerName() {
+        SyncMBeanImpl syncMBean = new SyncMBeanImpl(REPOSITORY, syncMgr, "invalid", idpMgr, idp.getName());
+        assertEquals("invalid", syncMBean.getSyncHandlerName());
+
+        // calling any sync-operation must fail due to the invalid configuration
+        try {
+            syncMBean.syncAllExternalUsers();
+            fail();
+        } catch (SyncRuntimeException e) {
+            //success
+        }
+    }
+
+    @Test
+    public void testGetIDPName() {
+        assertEquals(idp.getName(), syncMBean.getIDPName());
+    }
+
+    @Test
+    public void testInvalidIDPName() {
+        SyncMBeanImpl syncMBean = new SyncMBeanImpl(REPOSITORY, syncMgr, SYNC_NAME, idpMgr, "invalid");
+        assertEquals("invalid", syncMBean.getIDPName());
+
+        // calling any sync-operation must fail due to the invalid configuration
+        try {
+            syncMBean.syncAllExternalUsers();
+            fail();
+        } catch (SyncRuntimeException e) {
+            //success
+        }
+    }
+
+    @Test
+    public void testSyncUsers() {
+        // TODO
+    }
+
+    @Test
+    public void syncAllUsers() {
+        // TODO
+    }
+
+    @Test
+    public void testSyncExternalUsers() {
+        // TODO
+    }
+
+    @Test
+    public void TestSyncAllExternalUsers() {
+        // TODO
+    }
+
+    @Test
+    public void TestListOrphanedUsers() {
+        // TODO
+    }
+
+    @Test
+    public void testPurgeOrphanedUsers() {
+        // TODO
+    }
+}
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncDelegatee.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncDelegatee.java	(revision )
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncDelegatee.java	(revision )
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.spi.security.authentication.external.impl.jmx;
+
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+
+import org.apache.jackrabbit.api.JackrabbitRepository;
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.commons.json.JsonUtil;
+import org.apache.jackrabbit.oak.spi.security.authentication.SystemSubject;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncContext;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncException;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncedIdentity;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncResultImpl;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncedIdentity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+final class SyncDelegatee {
+
+    private static final Logger log = LoggerFactory.getLogger(SyncDelegatee.class);
+
+    private static final String ERROR_CREATE_DELEGATEE = "Unable to create delegatee";
+    private static final String ERROR_SYNC_USER = "Error while syncing user {}";
+
+    private final SyncHandler handler;
+    private final ExternalIdentityProvider idp;
+    private final UserManager userMgr;
+    private final Session systemSession;
+
+    private SyncContext context;
+
+    private SyncDelegatee(@Nonnull SyncHandler handler, @Nonnull ExternalIdentityProvider idp,
+                          @Nonnull JackrabbitSession systemSession) throws SyncException, RepositoryException {
+        this.handler = handler;
+        this.idp = idp;
+
+        this.systemSession = systemSession;
+        this.userMgr = systemSession.getUserManager();
+        this.context = handler.createContext(idp, userMgr, systemSession.getValueFactory());
+
+        log.info("Created delegatee for SyncMBean with session: {} {}", systemSession, systemSession.getUserID());
+    }
+
+    static SyncDelegatee createInstance(final @Nonnull Repository repository, @Nonnull SyncHandler handler, @Nonnull ExternalIdentityProvider idp) {
+        Session systemSession;
+        try {
+            systemSession = Subject.doAs(SystemSubject.INSTANCE, new PrivilegedExceptionAction<Session>() {
+                @Override
+                public Session run() throws RepositoryException {
+                    if (repository instanceof JackrabbitRepository) {
+                        // this is to bypass GuestCredentials injection in the "AbstractSlingRepository2"
+                        return ((JackrabbitRepository) repository).login(null, null, null);
+                    } else {
+                        return repository.login(null, null);
+                    }
+                }
+            });
+        } catch (PrivilegedActionException e) {
+            throw new SyncRuntimeException(ERROR_CREATE_DELEGATEE, e);
+        }
+
+        if (!(systemSession instanceof JackrabbitSession)) {
+            systemSession.logout();
+            throw new SyncRuntimeException("Unable to create SyncContext: JackrabbitSession required.");
+        }
+        try {
+            return new SyncDelegatee(handler, idp, (JackrabbitSession) systemSession);
+        } catch (RepositoryException e) {
+            systemSession.logout();
+            throw new SyncRuntimeException(ERROR_CREATE_DELEGATEE, e);
+        } catch (SyncException e) {
+            systemSession.logout();
+            throw new SyncRuntimeException(ERROR_CREATE_DELEGATEE, e);
+        }
+    }
+
+    void close() {
+        if (context != null) {
+            context.close();
+            context = null;
+        }
+        if (systemSession.isLive()) {
+            systemSession.logout();
+        }
+    }
+
+    /**
+     * @see SynchronizationMBean#syncUsers(String[], boolean)
+     */
+    @Nonnull
+    String[] syncUsers(@Nonnull String[] userIds, boolean purge) {
+        context.setKeepMissing(!purge)
+                .setForceGroupSync(true)
+                .setForceUserSync(true);
+        List<String> list = new ArrayList<String>();
+        for (String userId: userIds) {
+            try {
+                append(list, syncUser(userId));
+            } catch (SyncException e) {
+                log.warn(ERROR_SYNC_USER, userId, e);
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * @see SynchronizationMBean#syncAllUsers(boolean)
+     */
+    @Nonnull
+    String[] syncAllUsers(boolean purge) {
+        try {
+            List<String> list = new ArrayList<String>();
+            context.setKeepMissing(!purge)
+                    .setForceGroupSync(true)
+                    .setForceUserSync(true);
+            Iterator<SyncedIdentity> iter = handler.listIdentities(userMgr);
+            while (iter.hasNext()) {
+                SyncedIdentity id = iter.next();
+                if (isMyIDP(id)) {
+                    try {
+                        append(list, syncUser(id.getId()));
+                    } catch (SyncException e) {
+                        log.error(ERROR_SYNC_USER, id, e);
+                        append(list, id, e);
+                    }
+                }
+            }
+            return list.toArray(new String[list.size()]);
+        } catch (RepositoryException e) {
+            throw new IllegalStateException("Error retrieving users for syncing", e);
+        }
+    }
+
+    /**
+     * @see SynchronizationMBean#syncExternalUsers(String[])
+     */
+    @Nonnull
+    String[] syncExternalUsers(@Nonnull String[] externalIds) {
+        List<String> list = new ArrayList<String>();
+        context.setForceGroupSync(true).setForceUserSync(true);
+        for (String externalId: externalIds) {
+            ExternalIdentityRef ref = ExternalIdentityRef.fromString(externalId);
+            try {
+                ExternalIdentity id = idp.getIdentity(ref);
+                SyncResult r;
+                if (id != null) {
+                    r = syncUser(id);
+                } else {
+                    r = new DefaultSyncResultImpl(
+                            new DefaultSyncedIdentity("", ref, false, -1),
+                            SyncResult.Status.NO_SUCH_IDENTITY
+                    );
+                }
+                append(list, r);
+            } catch (ExternalIdentityException e) {
+                log.warn("error while fetching the external identity {}", externalId, e);
+                append(list, ref, e);
+            } catch (SyncException e) {
+                log.error(ERROR_SYNC_USER, ref, e);
+                append(list, ref, e);
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * @see SynchronizationMBean#syncAllExternalUsers()
+     */
+    @Nonnull
+    String[] syncAllExternalUsers() {
+        List<String> list = new ArrayList<String>();
+        context.setForceGroupSync(true).setForceUserSync(true);
+        try {
+            Iterator<ExternalUser> iter = idp.listUsers();
+            while (iter.hasNext()) {
+                ExternalUser user = iter.next();
+                try {
+                    SyncResult r = syncUser(user);
+                    if (r.getIdentity() == null) {
+                        r = new DefaultSyncResultImpl(
+                                new DefaultSyncedIdentity(user.getId(), user.getExternalId(), false, -1),
+                                SyncResult.Status.NO_SUCH_IDENTITY
+                        );
+                        log.warn("sync failed. {}", r.getIdentity());
+                    } else {
+                        log.info("synced {}", r.getIdentity());
+                    }
+                    append(list, r);
+                } catch (SyncException e) {
+                    log.error(ERROR_SYNC_USER, user, e);
+                    append(list, user.getExternalId(), e);
+                }
+            }
+            return list.toArray(new String[list.size()]);
+        } catch (ExternalIdentityException e) {
+            throw new SyncRuntimeException("Unable to retrieve external users", e);
+        }
+    }
+
+    /**
+     * @see SynchronizationMBean#listOrphanedUsers()
+     */
+    @Nonnull
+    String[] listOrphanedUsers() {
+        List<String> list = new ArrayList<String>();
+        try {
+            Iterator<SyncedIdentity> iter = handler.listIdentities(userMgr);
+            while (iter.hasNext()) {
+                SyncedIdentity id = iter.next();
+                if (isMyIDP(id)) {
+                    try {
+                        ExternalIdentityRef ref = id.getExternalIdRef();
+                        ExternalIdentity extId = (ref == null) ? null : idp.getIdentity(ref);
+                        if (extId == null) {
+                            list.add(id.getId());
+                        }
+                    } catch (ExternalIdentityException e) {
+                        log.error("Error while fetching external identity {}", id, e);
+                    }
+                }
+            }
+        } catch (RepositoryException e) {
+            log.error("Error while listing orphaned users", e);
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    /**
+     * @see SynchronizationMBean#purgeOrphanedUsers()
+     */
+    @Nonnull
+    String[] purgeOrphanedUsers() {
+        context.setKeepMissing(false);
+        List<String> list = new ArrayList<String>();
+        for (String userId: listOrphanedUsers()) {
+            try {
+                append(list, syncUser(userId));
+            } catch (SyncException e) {
+                log.warn(ERROR_SYNC_USER, userId, e);
+            }
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    //------------------------------------------------------------< private >---
+
+    private boolean isMyIDP(@Nonnull SyncedIdentity id) {
+        ExternalIdentityRef ref = id.getExternalIdRef();
+        String providerName = (ref == null) ? null : ref.getProviderName();
+        return providerName != null && (providerName.isEmpty() || providerName.equals(idp.getName()));
+    }
+
+
+    @Nonnull
+    private SyncResult syncUser(@Nonnull ExternalIdentity id) throws SyncException {
+        try {
+            SyncResult r = context.sync(id);
+            systemSession.save();
+            return r;
+        } catch (RepositoryException e) {
+            throw new SyncException(e);
+        }
+    }
+
+    @Nonnull
+    private SyncResult syncUser(@Nonnull String userId) throws SyncException {
+        try {
+            SyncResult r = context.sync(userId);
+            systemSession.save();
+            return r;
+        } catch (RepositoryException e) {
+            throw new SyncException(e);
+        }
+    }
+
+    private static void append(@Nonnull List<String> list, @Nonnull SyncResult r) {
+        SyncedIdentity syncedIdentity = r.getIdentity();
+        String uid = JsonUtil.getJsonString((syncedIdentity == null ? null : syncedIdentity.getId()));
+        ExternalIdentityRef externalIdentityRef = (syncedIdentity == null) ? null : syncedIdentity.getExternalIdRef();
+        String eid = (externalIdentityRef == null) ? "\"\"" : JsonUtil.getJsonString(externalIdentityRef.getString());
+        String jsonStr = String.format("{op:\"%s\",uid:%s,eid:%s}", getOperationFromStatus(r.getStatus()), uid, eid);
+        list.add(jsonStr);
+    }
+
+    private static void append(@Nonnull List<String> list, @Nonnull ExternalIdentityRef idRef, @Nonnull Exception e) {
+        String eid = JsonUtil.getJsonString(idRef.getString());
+        String msg = JsonUtil.getJsonString(e.toString());
+        String jsonStr = String.format("{op:\"ERR\",uid:\"\",eid:%s,msg:%s}", eid, msg);
+        list.add(jsonStr);
+    }
+
+    private static void append(@Nonnull List<String> list, @Nonnull SyncedIdentity id, @Nonnull Exception e) {
+        String uid = JsonUtil.getJsonString(id.getId());
+        ExternalIdentityRef ref = id.getExternalIdRef();
+        String eid = (ref == null) ? "\"\"" : JsonUtil.getJsonString(ref.getString());
+        String msg = JsonUtil.getJsonString(e.toString());
+        list.add(String.format("{op:\"ERR\",uid:%s,eid:%s,msg:%s}", uid, eid, msg));
+    }
+
+    private static String getOperationFromStatus(SyncResult.Status syncStatus) {
+        String op;
+        switch (syncStatus) {
+            case NOP:
+                op = "nop";
+                break;
+            case ADD:
+                op = "add";
+                break;
+            case UPDATE:
+                op = "upd";
+                break;
+            case DELETE:
+                op = "del";
+                break;
+            case NO_SUCH_AUTHORIZABLE:
+                op = "nsa";
+                break;
+            case NO_SUCH_IDENTITY:
+                op = "nsi";
+                break;
+            case MISSING:
+                op = "mis";
+                break;
+            case FOREIGN:
+                op = "for";
+                break;
+            default:
+                op = "";
+        }
+        return op;
+    }
+}
\ No newline at end of file
Index: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImpl.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImpl.java	(revision 1708794)
+++ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImpl.java	(revision )
@@ -16,42 +16,18 @@
  */
 package org.apache.jackrabbit.oak.spi.security.authentication.external.impl.jmx;
 
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
 import javax.annotation.Nonnull;
 import javax.jcr.Repository;
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-import javax.security.auth.Subject;
 
-import org.apache.jackrabbit.api.JackrabbitRepository;
-import org.apache.jackrabbit.api.JackrabbitSession;
-import org.apache.jackrabbit.api.security.user.UserManager;
-import org.apache.jackrabbit.commons.json.JsonUtil;
-import org.apache.jackrabbit.oak.spi.security.authentication.SystemSubject;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProviderManager;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncContext;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncException;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncedIdentity;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncResultImpl;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncedIdentity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * {@code SyncMBeanImpl}...
+ * Implementation of the {@link SynchronizationMBean} interface.
  */
 public class SyncMBeanImpl implements SynchronizationMBean {
 
@@ -77,300 +53,23 @@
     }
 
     @Nonnull
-    private Delegatee getDelegatee() {
+    private SyncDelegatee getDelegatee() {
         SyncHandler handler = syncManager.getSyncHandler(syncName);
         if (handler == null) {
             log.error("No sync manager available for name {}.", syncName);
-            throw new IllegalArgumentException("No sync manager available for name " + syncName);
+            throw new SyncRuntimeException("No sync manager available for name " + syncName);
         }
         ExternalIdentityProvider idp = idpManager.getProvider(idpName);
         if (idp == null) {
             log.error("No idp available for name", idpName);
-            throw new IllegalArgumentException("No idp manager available for name " + idpName);
+            throw new SyncRuntimeException("No idp manager available for name " + idpName);
         }
-        try {
-            return new Delegatee(handler, idp);
-        } catch (SyncException e) {
-            throw new IllegalArgumentException("Unable to create delegatee", e);
+        return SyncDelegatee.createInstance(repository, handler, idp);
-        }
+    }
-    }
 
-    private final class Delegatee {
 
-        private SyncHandler handler;
-
-        private ExternalIdentityProvider idp;
-
-        private SyncContext context;
-
-        private UserManager userMgr;
-
-        private Session systemSession;
-
-        private Delegatee(@Nonnull SyncHandler handler, @Nonnull ExternalIdentityProvider idp) throws SyncException {
-            this.handler = handler;
-            this.idp = idp;
-            try {
-                systemSession = Subject.doAs(SystemSubject.INSTANCE, new PrivilegedExceptionAction<Session>() {
-                    @Override
-                    public Session run() throws RepositoryException {
-                        if (repository instanceof JackrabbitRepository) {
-                            // this is to bypass GuestCredentials injection in the "AbstractSlingRepository2"
-                            return ((JackrabbitRepository) repository).login(null, null, null);
-                        } else {
-                            return repository.login(null, null);
-                        }
-                    }
-                });
-            } catch (PrivilegedActionException e) {
-                throw new SyncException(e);
-            }
-            try {
-                context = handler.createContext(idp, userMgr = ((JackrabbitSession) systemSession).getUserManager(), systemSession.getValueFactory());
-            } catch (Exception e) {
-                systemSession.logout();
-                throw new SyncException(e);
-            }
-
-            log.info("Created delegatee for SyncMBean with session: {} {}", systemSession, systemSession.getUserID());
-        }
-
-        private void close() {
-            if (context != null) {
-                context.close();
-                context = null;
-            }
-            if (systemSession != null) {
-                systemSession.logout();
-            }
-        }
-
-        /**
-         * @see SynchronizationMBean#syncUsers(String[], boolean)
-         */
+    //-----------------------------------------------< SynchronizationMBean >---
-        @Nonnull
+    @Nonnull
-        public String[] syncUsers(@Nonnull String[] userIds, boolean purge) {
-            context.setKeepMissing(!purge)
-                    .setForceGroupSync(true)
-                    .setForceUserSync(true);
-            List<String> result = new ArrayList<String>();
-            for (String userId: userIds) {
-                try {
-                    SyncResult r = context.sync(userId);
-                    systemSession.save();
-                    result.add(getJSONString(r));
-                } catch (Exception e) {
-                    log.warn("Error while syncing user {}", userId, e);
-                }
-            }
-            return result.toArray(new String[result.size()]);
-        }
-
-        /**
-         * @see SynchronizationMBean#syncAllUsers(boolean)
-         */
-        @Nonnull
-        public String[] syncAllUsers(boolean purge) {
-            try {
-                List<String> list = new ArrayList<String>();
-                context.setKeepMissing(!purge)
-                        .setForceGroupSync(true)
-                        .setForceUserSync(true);
-                Iterator<SyncedIdentity> iter = handler.listIdentities(userMgr);
-                while (iter.hasNext()) {
-                    SyncedIdentity id = iter.next();
-                    if (isMyIDP(id)) {
-                        try {
-                            SyncResult r = context.sync(id.getId());
-                            systemSession.save();
-                            list.add(getJSONString(r));
-                        } catch (Exception e) {
-                            log.error("Error while syncing user {}", id, e);
-                            list.add(getJSONString(id, e));
-                        }
-                    }
-                }
-                return list.toArray(new String[list.size()]);
-            } catch (RepositoryException e) {
-                throw new IllegalStateException("Error retrieving users for syncing", e);
-            }
-        }
-
-        /**
-         * @see SynchronizationMBean#syncExternalUsers(String[])
-         */
-        @Nonnull
-        public String[] syncExternalUsers(@Nonnull String[] externalIds) {
-            List<String> list = new ArrayList<String>();
-            context.setForceGroupSync(true).setForceUserSync(true);
-            for (String externalId: externalIds) {
-                ExternalIdentityRef ref = ExternalIdentityRef.fromString(externalId);
-                try {
-                    ExternalIdentity id = idp.getIdentity(ref);
-                    if (id != null) {
-                        SyncResult r = context.sync(id);
-                        systemSession.save();
-                        list.add(getJSONString(r));
-                    } else {
-                        SyncResult r = new DefaultSyncResultImpl(
-                                new DefaultSyncedIdentity("", ref, false, -1),
-                                SyncResult.Status.NO_SUCH_IDENTITY
-                        );
-                        list.add(getJSONString(r));
-                    }
-                } catch (ExternalIdentityException e) {
-                    log.warn("error while fetching the external identity {}", externalId, e);
-                    list.add(getJSONString(ref, e));
-                } catch (Exception e) {
-                    log.error("Error while syncing user {}", ref, e);
-                    list.add(getJSONString(ref, e));
-                }
-            }
-            return list.toArray(new String[list.size()]);
-        }
-
-        /**
-         * @see SynchronizationMBean#syncAllExternalUsers()
-         */
-        @Nonnull
-        public String[] syncAllExternalUsers() {
-            List<String> list = new ArrayList<String>();
-            context.setForceGroupSync(true).setForceUserSync(true);
-            try {
-                Iterator<ExternalUser> iter = idp.listUsers();
-                while (iter.hasNext()) {
-                    ExternalUser user = iter.next();
-                    try {
-                        SyncResult r = context.sync(user);
-                        systemSession.save();
-                        if (r.getIdentity() == null) {
-                            r = new DefaultSyncResultImpl(
-                                    new DefaultSyncedIdentity(user.getId(), user.getExternalId(), false, -1),
-                                    SyncResult.Status.NO_SUCH_IDENTITY
-                            );
-                            log.warn("sync failed. {}", r.getIdentity());
-                        } else {
-                            log.info("synced {}", r.getIdentity());
-                        }
-                        list.add(getJSONString(r));
-                    } catch (Exception e) {
-                        log.error("Error while syncing user {}", user, e);
-                        list.add(getJSONString(user.getExternalId(), e));
-                    }
-                }
-                return list.toArray(new String[list.size()]);
-            } catch (ExternalIdentityException e) {
-                throw new IllegalArgumentException("Unable to retrieve external users", e);
-            }
-        }
-
-        /**
-         * @see SynchronizationMBean#listOrphanedUsers()
-         */
-        @Nonnull
-        public String[] listOrphanedUsers() {
-            List<String> list = new ArrayList<String>();
-            try {
-                Iterator<SyncedIdentity> iter = handler.listIdentities(userMgr);
-                while (iter.hasNext()) {
-                    SyncedIdentity id = iter.next();
-                    if (isMyIDP(id)) {
-                        try {
-                            ExternalIdentityRef ref = id.getExternalIdRef();
-                            ExternalIdentity extId = ref == null ? null : idp.getIdentity(ref);
-                            if (extId == null) {
-                                list.add(id.getId());
-                            }
-                        } catch (Exception e) {
-                            log.error("Error while fetching external identity {}", id, e);
-                        }
-                    }
-                }
-            } catch (RepositoryException e) {
-                log.error("Error while listing orphaned users", e);
-            }
-            return list.toArray(new String[list.size()]);
-        }
-
-        /**
-         * @see SynchronizationMBean#purgeOrphanedUsers()
-         */
-        @Nonnull
-        public String[] purgeOrphanedUsers() {
-            context.setKeepMissing(false);
-            List<String> result = new ArrayList<String>();
-            for (String userId: listOrphanedUsers()) {
-                try {
-                    SyncResult r = context.sync(userId);
-                    systemSession.save();
-                    result.add(getJSONString(r));
-                } catch (Exception e) {
-                    log.warn("Error while syncing user {}", userId, e);
-                }
-            }
-            return result.toArray(new String[result.size()]);
-        }
-
-        private boolean isMyIDP(@Nonnull SyncedIdentity id) {
-            String providerName = id.getExternalIdRef() == null
-                    ? null
-                    : id.getExternalIdRef().getProviderName();
-            return providerName != null && (providerName.isEmpty() || providerName.equals(idp.getName()));
-        }
-    }
-
-    private static String getJSONString(@Nonnull SyncResult r) {
-        String op;
-        switch (r.getStatus()) {
-            case NOP:
-                op = "nop";
-                break;
-            case ADD:
-                op = "add";
-                break;
-            case UPDATE:
-                op = "upd";
-                break;
-            case DELETE:
-                op = "del";
-                break;
-            case NO_SUCH_AUTHORIZABLE:
-                op = "nsa";
-                break;
-            case NO_SUCH_IDENTITY:
-                op = "nsi";
-                break;
-            case MISSING:
-                op = "mis";
-                break;
-            case FOREIGN:
-                op = "for";
-                break;
-            default:
-                op = "";
-        }
-        SyncedIdentity syncedIdentity = r.getIdentity();
-        String uid = JsonUtil.getJsonString((syncedIdentity == null ? null : syncedIdentity.getId()));
-        ExternalIdentityRef externalIdentityRef = (syncedIdentity == null) ? null : syncedIdentity.getExternalIdRef();
-        String eid = (externalIdentityRef == null) ? "\"\"" : JsonUtil.getJsonString(externalIdentityRef.getString());
-        return String.format("{op:\"%s\",uid:%s,eid:%s}", op, uid, eid);
-    }
-
-    private static String getJSONString(@Nonnull SyncedIdentity id, @Nonnull Exception e) {
-        String uid = JsonUtil.getJsonString(id.getId());
-        String eid = (id.getExternalIdRef() == null) ? "\"\"" : JsonUtil.getJsonString(id.getExternalIdRef().getString());
-        String msg = JsonUtil.getJsonString(e.toString());
-        return String.format("{op:\"ERR\",uid:%s,eid:%s,msg:%s}", uid, eid, msg);
-    }
-
-    private static String getJSONString(ExternalIdentityRef ref, Exception e) {
-        String eid = JsonUtil.getJsonString(ref.getString());
-        String msg = JsonUtil.getJsonString(e.toString());
-        return String.format("{op:\"ERR\",uid:\"\",eid:%s,msg:%s}", eid, msg);
-    }
-
-    //---------------------------------------------------------------------------------------< SynchronizationMBean >---
-    @Nonnull
     @Override
     public String getSyncHandlerName() {
         return syncName;
@@ -385,7 +84,7 @@
     @Nonnull
     @Override
     public String[] syncUsers(@Nonnull String[] userIds, boolean purge) {
-        Delegatee delegatee = getDelegatee();
+        SyncDelegatee delegatee = getDelegatee();
         try {
             return delegatee.syncUsers(userIds, purge);
         } finally {
@@ -396,7 +95,7 @@
     @Nonnull
     @Override
     public String[] syncAllUsers(boolean purge) {
-        Delegatee delegatee = getDelegatee();
+        SyncDelegatee delegatee = getDelegatee();
         try {
             return delegatee.syncAllUsers(purge);
         } finally {
@@ -407,7 +106,7 @@
     @Nonnull
     @Override
     public String[] syncExternalUsers(@Nonnull String[] externalIds) {
-        Delegatee delegatee = getDelegatee();
+        SyncDelegatee delegatee = getDelegatee();
         try {
             return delegatee.syncExternalUsers(externalIds);
         } finally {
@@ -418,7 +117,7 @@
     @Nonnull
     @Override
     public String[] syncAllExternalUsers() {
-        Delegatee delegatee = getDelegatee();
+        SyncDelegatee delegatee = getDelegatee();
         try {
             return delegatee.syncAllExternalUsers();
         } finally {
@@ -429,7 +128,7 @@
     @Nonnull
     @Override
     public String[] listOrphanedUsers() {
-        Delegatee delegatee = getDelegatee();
+        SyncDelegatee delegatee = getDelegatee();
         try {
             return delegatee.listOrphanedUsers();
         } finally {
@@ -440,7 +139,7 @@
     @Nonnull
     @Override
     public String[] purgeOrphanedUsers() {
-        Delegatee delegatee = getDelegatee();
+        SyncDelegatee delegatee = getDelegatee();
         try {
             return delegatee.purgeOrphanedUsers();
         } finally {
\ No newline at end of file
