Introduce new extension point LdapContextProvider

LDAPUserComponent created LDAP contexts from an LDAPConfig
configuration, which contained sensitive information (user/password)
of the LDAP backend. For certain hosting environmemts this is not
acceptable, but instead credentials must be stored safely outside
a running Skalli instance.

Therefore, the LDAPClient helper class has been removed. Query
related algorithms have been moved to LDAPUserComponent. The
context creation has been moved to DefaultLdapContextProvider,
which implements a new extension point LdapContextProvider.
DefaultLdapContextProvider still creates LDAP contexts based
on configuration information and is retaind for environments,
where storing credentials in the Skalli database is acceptable.

Consequently, configuration attributes related to the LDAP
backend (like url, credentials etc.) have been moved from
LDAPConfig to a new configuration section LDAPServerConfig.

Change-Id: I19e6bf09192fab3a97d36b579106de737ff1ceb5
Signed-off-by: Michael Ochmann <michael.ochmann@sap.com
diff --git a/org.eclipse.skalli.api/META-INF/MANIFEST.MF b/org.eclipse.skalli.api/META-INF/MANIFEST.MF
index 0bc220d..d14dfe1 100644
--- a/org.eclipse.skalli.api/META-INF/MANIFEST.MF
+++ b/org.eclipse.skalli.api/META-INF/MANIFEST.MF
@@ -81,6 +81,7 @@
  org.eclipse.skalli.services.tagging,
  org.eclipse.skalli.services.template,
  org.eclipse.skalli.services.user,
+ org.eclipse.skalli.services.user.ldap,
  org.eclipse.skalli.services.validation
 Service-Component: OSGI-INF/ExtensionServicesComponent.xml,
  OSGI-INF/EntityServicesComponent.xml,
diff --git a/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/user/ldap/LdapContextProvider.java b/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/user/ldap/LdapContextProvider.java
new file mode 100644
index 0000000..2c72b17
--- /dev/null
+++ b/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/user/ldap/LdapContextProvider.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (c) 2010-2016 SAP AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     SAP AG - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.skalli.services.user.ldap;
+
+import javax.naming.AuthenticationException;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapContext;
+
+import org.eclipse.skalli.services.user.UserService;
+
+/**
+ * Extension point for LDAP-based {@link UserService} implementations.
+ */
+public interface LdapContextProvider {
+
+    /**
+     * Returns a unique identifier for this LDAP context provider.
+     */
+    public String getId();
+
+    /**
+     * Returns a pre-initialized LDAP context instance. Note, a provider
+     * must return a new context instance every time this method is called,
+     * since {@LdapContext} usually is not thread-safe.
+     *
+     * @throws AuthenticationException if the provider could not authenticate with the LDAP backend.
+     * @throws NamingException if the creation of the LDAP context failed, for example due to
+     * insufficent configuration configuration or because a connection to the LDAP backend could
+     * not be established.
+     */
+    public LdapContext getLdapContext(String destination) throws NamingException, AuthenticationException;
+
+}
diff --git a/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPClientTest.java b/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPClientTest.java
deleted file mode 100644
index 10094b4..0000000
--- a/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPClientTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2010-2014 SAP AG and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     SAP AG - initial API and implementation
- *******************************************************************************/
-package org.eclipse.skalli.core.user.ldap;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import javax.naming.directory.DirContext;
-import javax.naming.directory.SearchControls;
-import javax.naming.directory.SearchResult;
-
-import org.easymock.EasyMock;
-import org.eclipse.skalli.model.User;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-@SuppressWarnings("nls")
-public class LDAPClientTest {
-
-    private static final String USERS_GROUP = "CN=Users,DC=some,DC=corp";
-
-    @Before
-    public void setUp() throws Exception {
-    }
-
-    @Test
-    public void testSearchUserById_nonExisting() throws Exception {
-        LDAPClient client = getLDAPClient();
-        DirContext mockContext = MockInitialDirContextFactory.getLatestMockContext();
-
-        EasyMock.reset(mockContext);
-
-        mockContext.close();
-        EasyMock.expectLastCall();
-
-        mockContext.search(EasyMock.eq(USERS_GROUP), EasyMock.isA(String.class), EasyMock
-                .isA(SearchControls.class));
-        EasyMock.expectLastCall().andReturn(new MockNamingEnumeration<SearchResult>());
-
-        EasyMock.replay(mockContext);
-
-        User user = client.searchUserById("non-existing-userid");
-        Assert.assertNotNull(user);
-        Assert.assertEquals("non-existing-userid", user.getDisplayName());
-
-        EasyMock.verify(mockContext);
-    }
-
-    @Test
-    public void testSearchUsersById_nonExisting() throws Exception {
-        LDAPClient client = getLDAPClient();
-        DirContext mockContext = MockInitialDirContextFactory.getLatestMockContext();
-
-        EasyMock.reset(mockContext);
-
-        mockContext.close();
-        EasyMock.expectLastCall();
-
-        mockContext.search(EasyMock.eq(USERS_GROUP), EasyMock.isA(String.class), EasyMock
-                .isA(SearchControls.class));
-        EasyMock.expectLastCall().andReturn(new MockNamingEnumeration<SearchResult>()).anyTimes();
-
-        EasyMock.replay(mockContext);
-
-        Set<String> ids = new HashSet<String>();
-        ids.add("id1");
-        ids.add("id2");
-
-        Set<User> user = client.searchUsersByIds(ids);
-        Assert.assertNotNull(user);
-        Assert.assertEquals(user.size(), 2);
-
-        EasyMock.verify(mockContext);
-    }
-
-    private LDAPClient getLDAPClient() {
-        LDAPConfig config = new LDAPConfig();
-        config.setCtxFactory(MockInitialDirContextFactory.class.getName());
-        config.setProviderUrl("ldap://thisIsIgnoredInTests");
-        config.setUsername("hello");
-        config.setPassword("world");
-        config.setBaseDN(USERS_GROUP);
-        return new LDAPClient(config);
-    }
-
-}
diff --git a/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/MockInitialDirContextFactory.java b/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/MockInitialDirContextFactory.java
deleted file mode 100644
index d24b41c..0000000
--- a/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/MockInitialDirContextFactory.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2010-2014 SAP AG and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     SAP AG - initial API and implementation
- *******************************************************************************/
-package org.eclipse.skalli.core.user.ldap;
-
-import java.util.Hashtable;
-
-import javax.naming.Context;
-import javax.naming.NamingException;
-import javax.naming.directory.DirContext;
-import javax.naming.spi.InitialContextFactory;
-
-import org.easymock.EasyMock;
-
-public class MockInitialDirContextFactory implements InitialContextFactory {
-
-    private static DirContext mockContext = null;
-
-    public static DirContext getLatestMockContext() {
-        return getOrCreateMockContext();
-    }
-
-    public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException {
-        return getOrCreateMockContext();
-    }
-
-    private static DirContext getOrCreateMockContext() {
-        synchronized (MockInitialDirContextFactory.class) {
-            if (mockContext == null) {
-                mockContext = (DirContext) EasyMock.createMock(DirContext.class);
-            }
-        }
-        return mockContext;
-    }
-
-}
diff --git a/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/MockNamingEnumeration.java b/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/MockNamingEnumeration.java
deleted file mode 100644
index 1eab540..0000000
--- a/org.eclipse.skalli.core.test/src/main/java/org/eclipse/skalli/core/user/ldap/MockNamingEnumeration.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2010-2014 SAP AG and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     SAP AG - initial API and implementation
- *******************************************************************************/
-package org.eclipse.skalli.core.user.ldap;
-
-import java.util.Iterator;
-import java.util.List;
-
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-
-public class MockNamingEnumeration<SearchResult> implements NamingEnumeration<Object> {
-
-    private Iterator<SearchResult> iterator = null;
-
-    public MockNamingEnumeration() {
-        this(null);
-    }
-
-    public MockNamingEnumeration(List<SearchResult> results) {
-        if (results != null) {
-            iterator = results.iterator();
-        }
-    }
-
-    public void close() throws NamingException {
-    }
-
-    public boolean hasMore() throws NamingException {
-        return hasMoreElements();
-    }
-
-    public SearchResult next() throws NamingException {
-        return nextElement();
-    }
-
-    public boolean hasMoreElements() {
-        if (iterator == null)
-            return false;
-        return iterator.hasNext();
-    }
-
-    public SearchResult nextElement() {
-        if (iterator == null)
-            return null;
-        return iterator.next();
-    }
-}
diff --git a/org.eclipse.skalli.core/META-INF/MANIFEST.MF b/org.eclipse.skalli.core/META-INF/MANIFEST.MF
index 2b8cf3d..492ddc6 100644
--- a/org.eclipse.skalli.core/META-INF/MANIFEST.MF
+++ b/org.eclipse.skalli.core/META-INF/MANIFEST.MF
@@ -77,6 +77,7 @@
  org.eclipse.skalli.services.tagging,
  org.eclipse.skalli.services.template,
  org.eclipse.skalli.services.user,
+ org.eclipse.skalli.services.user.ldap,
  org.eclipse.skalli.services.validation,
  org.jdom,
  org.jsoup,
@@ -124,6 +125,7 @@
  OSGI-INF/LocalUserComponent.xml,
  OSGI-INF/LDAPConfigSection.xml,
  OSGI-INF/LDAPUserComponent.xml,
+ OSGI-INF/DefaultLdapContextProvider.xml,
  OSGI-INF/LocalGroupComponent.xml,
  OSGI-INF/LocalRoleComponent.xml,
  OSGI-INF/ExtensionServiceCore.xml,
diff --git a/org.eclipse.skalli.core/OSGI-INF/DefaultLdapContextProvider.xml b/org.eclipse.skalli.core/OSGI-INF/DefaultLdapContextProvider.xml
new file mode 100755
index 0000000..b451c3e
--- /dev/null
+++ b/org.eclipse.skalli.core/OSGI-INF/DefaultLdapContextProvider.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright (c) 2010-2014 SAP AG and others.
+    All rights reserved. This program and the accompanying materials
+    are made available under the terms of the Eclipse Public License v1.0
+    which accompanies this distribution, and is available at
+    http://www.eclipse.org/legal/epl-v10.html
+
+    Contributors:
+        SAP AG - initial API and implementation
+ -->
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true"
+        name="org.eclipse.skalli.core.config.ldap.ctxProvider.default">
+    <implementation class="org.eclipse.skalli.core.user.ldap.DefaultLdapContextProvider"/>
+    <service>
+        <provide interface="org.eclipse.skalli.services.user.ldap.LdapContextProvider"/>
+    </service>
+</scr:component>
+
diff --git a/org.eclipse.skalli.core/OSGI-INF/LDAPUserComponent.xml b/org.eclipse.skalli.core/OSGI-INF/LDAPUserComponent.xml
index 48f0659..64a2591 100644
--- a/org.eclipse.skalli.core/OSGI-INF/LDAPUserComponent.xml
+++ b/org.eclipse.skalli.core/OSGI-INF/LDAPUserComponent.xml
@@ -15,6 +15,13 @@
         <provide interface="org.eclipse.skalli.services.user.UserService"/>
     </service>
     <reference
+        name="LdapContextProvider"
+        interface="org.eclipse.skalli.services.user.ldap.LdapContextProvider"
+        cardinality="0..n"
+        policy="dynamic"
+        bind="bindLdapContextProvider"
+        unbind="unbindLdapContextProvider"/>
+    <reference
         name="EventService"
         interface="org.eclipse.skalli.services.event.EventService"
         cardinality="0..1"
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/DefaultLdapContextProvider.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/DefaultLdapContextProvider.java
new file mode 100644
index 0000000..c2670dc
--- /dev/null
+++ b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/DefaultLdapContextProvider.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) 2010-2016 SAP AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     SAP AG - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.skalli.core.user.ldap;
+
+import java.text.MessageFormat;
+import java.util.Hashtable;
+
+import javax.naming.AuthenticationException;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+
+import org.apache.commons.lang.BooleanUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.math.NumberUtils;
+import org.eclipse.skalli.core.destination.DestinationConfig;
+import org.eclipse.skalli.core.destination.DestinationsConfig;
+import org.eclipse.skalli.services.configuration.Configurations;
+import org.eclipse.skalli.services.user.ldap.LdapContextProvider;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.component.ComponentConstants;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.jndi.JNDIConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultLdapContextProvider implements LdapContextProvider {
+
+    private final static Logger LOG = LoggerFactory.getLogger(DefaultLdapContextProvider.class);
+
+    private static final String LDAPS = "ldaps://"; //$NON-NLS-1$
+    private static final String SIMPLE_AUTHENTICATION = "simple"; //$NON-NLS-1$
+    private static final String NO_SSL_VERIFY = "NO_SSL_VERIFY"; //$NON-NLS-1$
+    private static final String JNDI_SOCKET_FACTORY = "java.naming.ldap.factory.socket"; //$NON-NLS-1$
+    private static final String DEFAULT_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; //$NON-NLS-1$
+    private static final String READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout";//$NON-NLS-1$
+    private static final String USE_CONNECTION_POOLING = "com.sun.jndi.ldap.connect.pool"; //$NON-NLS-1$
+
+    private static final long DEFAULT_READ_TIMEOUT = 30000L;
+
+    protected void activate(ComponentContext context) {
+        LOG.info(MessageFormat.format("[LdapContextProvider][default] {0} : activated", //$NON-NLS-1$
+                (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
+    }
+
+    protected void deactivate(ComponentContext context) {
+        LOG.info(MessageFormat.format("[LdapContextProvider][default] {0} : deactivated", //$NON-NLS-1$
+                (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
+    }
+
+    @Override
+    public String getId() {
+        return "default"; //$NON-NLS-1$
+    }
+
+    @Override
+    public LdapContext getLdapContext(String destination) throws NamingException, AuthenticationException {
+        DestinationsConfig destinationsConfig = Configurations.getConfiguration(DestinationsConfig.class);
+        if (destinationsConfig == null) {
+            throw new NamingException("No LDAP destination available");
+        }
+        DestinationConfig destinationConfig = destinationsConfig.getDestination(destination);
+        if (destinationConfig == null) {
+            throw new NamingException("No LDAP destination available");
+        }
+        String providerUrl = destinationConfig.getUrlPattern();
+        if (StringUtils.isBlank(providerUrl)) {
+            throw new NamingException("No LDAP server configured");
+        }
+        if (StringUtils.isBlank(destinationConfig.getUser()) || StringUtils.isBlank(destinationConfig.getPassword())) {
+            throw new AuthenticationException("No LDAP credentials available");
+        }
+        String ctxFactory = destinationConfig.getProperty(Context.INITIAL_CONTEXT_FACTORY);
+        if (StringUtils.isBlank(ctxFactory)) {
+            ctxFactory = DEFAULT_CONTEXT_FACTORY;
+        }
+        String authentication = destinationConfig.getAuthentication();
+        if (StringUtils.isBlank(authentication)) {
+            authentication = SIMPLE_AUTHENTICATION;
+        }
+
+        Hashtable<String, Object> env = new Hashtable<String, Object>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, ctxFactory);
+        env.put(Context.PROVIDER_URL, providerUrl);
+        env.put(Context.SECURITY_PRINCIPAL, destinationConfig.getUser());
+        env.put(Context.SECURITY_CREDENTIALS, destinationConfig.getPassword());
+        env.put(Context.SECURITY_AUTHENTICATION, authentication);
+        String referral = destinationConfig.getProperty(Context.REFERRAL);
+        if (StringUtils.isNotBlank(referral)) {
+            env.put(Context.REFERRAL, referral);
+        }
+        if (providerUrl.startsWith(LDAPS)) {
+            env.put(Context.SECURITY_PROTOCOL, "ssl"); //$NON-NLS-1$
+            if (BooleanUtils.toBoolean(destinationConfig.getProperty(NO_SSL_VERIFY))) {
+                env.put(JNDI_SOCKET_FACTORY, LDAPTrustAllSocketFactory.class.getName());
+            }
+        }
+        // Gemini-specific properties
+        env.put(JNDIConstants.BUNDLE_CONTEXT,
+                FrameworkUtil.getBundle(DefaultLdapContextProvider.class).getBundleContext());
+
+        // com.sun.jndi.ldap.LdapCtxFactory specific properties
+        long readTimeout = NumberUtils.toLong(destinationConfig.getProperty(READ_TIMEOUT), DEFAULT_READ_TIMEOUT);
+        env.put(READ_TIMEOUT, Long.toString(readTimeout));
+        env.put(USE_CONNECTION_POOLING, "true"); //$NON-NLS-1$
+
+        // extremly ugly classloading workaround:
+        // com.sun.jndi.ldap.LdapCtxFactory uses Class.forName() to load the socket factory, shame on them!
+        InitialLdapContext ctx = null;
+        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
+        try {
+            Thread.currentThread().setContextClassLoader(LDAPTrustAllSocketFactory.class.getClassLoader());
+            ctx = new InitialLdapContext(env, null);
+        } finally {
+            if (classloader != null) {
+                Thread.currentThread().setContextClassLoader(classloader);
+            }
+        }
+        return ctx;
+    }
+}
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPClient.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPClient.java
deleted file mode 100644
index 18f871a..0000000
--- a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPClient.java
+++ /dev/null
@@ -1,404 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2010-2014 SAP AG and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
- * which accompanies this distribution, and is available at
- * http://www.eclipse.org/legal/epl-v10.html
- *
- * Contributors:
- *     SAP AG - initial API and implementation
- *******************************************************************************/
-package org.eclipse.skalli.core.user.ldap;
-
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Set;
-
-import javax.naming.AuthenticationException;
-import javax.naming.Context;
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.SizeLimitExceededException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.SearchControls;
-import javax.naming.directory.SearchResult;
-import javax.naming.ldap.InitialLdapContext;
-import javax.naming.ldap.LdapContext;
-
-import org.apache.commons.lang.StringUtils;
-import org.eclipse.skalli.commons.CollectionUtils;
-import org.eclipse.skalli.core.user.NormalizeUtil;
-import org.eclipse.skalli.model.User;
-import org.osgi.framework.FrameworkUtil;
-import org.osgi.service.jndi.JNDIConstants;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class LDAPClient {
-
-    private final static Logger LOG = LoggerFactory.getLogger(LDAPClient.class);
-
-    private static final String LDAPS_SCHEME = "ldaps:"; //$NON-NLS-1$
-    private static final String SIMPLE_AUTHENTICATION = "simple"; //$NON-NLS-1$
-    private static final String JNDI_SOCKET_FACTORY = "java.naming.ldap.factory.socket"; //$NON-NLS-1$
-    private static final String DEFAULT_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; //$NON-NLS-1$
-    private static final String READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout";//$NON-NLS-1$
-    private static final String USE_CONNECTION_POOLING = "com.sun.jndi.ldap.connect.pool"; //$NON-NLS-1$
-    private static final String CONNECT_POOL_PROTOCOLS = "com.sun.jndi.ldap.connect.pool.protocol"; //$NON-NLS-1$
-    private static final String CONNECT_POOL_DEBUG = "com.sun.jndi.ldap.connect.pool.debug"; //$NON-NLS-1$
-    private static final String CONNECT_POOL_MAXSIZE = "com.sun.jndi.ldap.connect.pool.maxsize"; //$NON-NLS-1$
-    private static final String CONNECT_POOL_TIMEOUT = "com.sun.jndi.ldap.connect.pool.timeout"; //$NON-NLS-1$
-
-    private static final String DEFAULT_READ_TIMEOUT = "30000"; //$NON-NLS-1$
-    private static final String DEFAULT_CONNECT_TIMEOUT = "10000"; //$NON-NLS-1$
-
-    private static final String DEFAULT_POOL_MAXSIZE = "5"; //$NON-NLS-1$
-    private static final String DEFAULT_POOL_PROTOCOLS = "plain ssl"; //$NON-NLS-1$
-
-    static {
-        // heaven knows why the following params are system properties while all other params can be set per context...
-        System.setProperty(CONNECT_POOL_PROTOCOLS, DEFAULT_POOL_PROTOCOLS);
-        System.setProperty(CONNECT_POOL_MAXSIZE, DEFAULT_POOL_MAXSIZE);
-        System.setProperty(CONNECT_POOL_TIMEOUT, DEFAULT_CONNECT_TIMEOUT);
-        if (LOG.isDebugEnabled()) {
-            System.setProperty(CONNECT_POOL_DEBUG, "fine"); //$NON-NLS-1$
-        }
-    }
-
-    private LDAPConfig config;
-
-    public LDAPClient(LDAPConfig config) {
-        this.config = config;
-    }
-
-    private LdapContext getLdapContext() throws NamingException, AuthenticationException {
-        if (config == null) {
-            throw new NamingException("LDAP not configured");
-        }
-        if (StringUtils.isBlank(config.getProviderUrl())) {
-            throw new NamingException("No LDAP server available");
-        }
-        if (StringUtils.isBlank(config.getUsername()) || StringUtils.isBlank(config.getPassword())) {
-            throw new AuthenticationException("No LDAP credentials available");
-        }
-        String ctxFactory = config.getCtxFactory();
-        if (StringUtils.isBlank(ctxFactory)) {
-            ctxFactory = DEFAULT_CONTEXT_FACTORY;
-        }
-        String authentication = config.getAuthentication();
-        if (StringUtils.isBlank(authentication)) {
-            authentication = SIMPLE_AUTHENTICATION;
-        }
-
-        Hashtable<String, Object> env = new Hashtable<String, Object>();
-        env.put(Context.INITIAL_CONTEXT_FACTORY, ctxFactory);
-        env.put(Context.PROVIDER_URL, config.getProviderUrl());
-        env.put(Context.SECURITY_PRINCIPAL, config.getUsername());
-        env.put(Context.SECURITY_CREDENTIALS, config.getPassword());
-        env.put(Context.SECURITY_AUTHENTICATION, authentication);
-        if (StringUtils.isNotBlank(config.getReferral())) {
-            env.put(Context.REFERRAL, config.getReferral());
-        }
-        if (config.getProviderUrl().startsWith(LDAPS_SCHEME)) {
-            env.put(Context.SECURITY_PROTOCOL, "ssl"); //$NON-NLS-1$
-            if (config.isSslNoVerify()) {
-                env.put(JNDI_SOCKET_FACTORY, LDAPTrustAllSocketFactory.class.getName());
-            }
-        }
-        // Gemini-specific properties
-        env.put(JNDIConstants.BUNDLE_CONTEXT,
-                FrameworkUtil.getBundle(LDAPClient.class).getBundleContext());
-
-        // com.sun.jndi.ldap.LdapCtxFactory specific properties
-        env.put(READ_TIMEOUT, DEFAULT_READ_TIMEOUT);
-        env.put(USE_CONNECTION_POOLING, "true"); //$NON-NLS-1$
-
-        // extremly ugly classloading workaround:
-        // com.sun.jndi.ldap.LdapCtxFactory uses Class.forName() to load the socket factory, shame on them!
-        InitialLdapContext ctx = null;
-        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
-        try {
-            Thread.currentThread().setContextClassLoader(LDAPTrustAllSocketFactory.class.getClassLoader());
-            ctx = new InitialLdapContext(env, null);
-        } finally {
-            if (classloader != null) {
-                Thread.currentThread().setContextClassLoader(classloader);
-            }
-        }
-        return ctx;
-    }
-
-    private void closeQuietly(LdapContext ldap) {
-        if (ldap != null) {
-            try {
-                ldap.close();
-            } catch (NamingException e) {
-                LOG.error("Failed to close LDAP connection", e);
-            }
-        }
-    }
-
-    private void closeQuietly(NamingEnumeration<?> result) {
-        if (result != null) {
-            try {
-                result.close();
-            } catch (NamingException e) {
-                LOG.error("Failed to close LDAP result set", e);
-            }
-        }
-    }
-
-    public User searchUserById(String userId) {
-        LdapContext ldap = null;
-        try {
-            ldap = getLdapContext();
-            return searchUserById(ldap, userId);
-        }  catch (Exception e) {
-            LOG.debug(MessageFormat.format("Failed to retrieve user ''{0}''", userId), e);
-            return new User(userId);
-        } finally {
-            closeQuietly(ldap);
-        }
-    }
-
-    public Set<User> searchUsersByIds(Set<String> userIds) {
-        LdapContext ldap = null;
-        try {
-            Set<User> ret = new HashSet<User>();
-            ldap = getLdapContext();
-            for (String userId : userIds) {
-                ret.add(searchUserById(ldap, userId));
-            }
-            return ret;
-        } catch (Exception e) {
-            LOG.debug(MessageFormat.format("Failed to retrieve users {0}",
-                    CollectionUtils.toString(userIds, ',')), e);
-            return Collections.emptySet();
-        } finally {
-            closeQuietly(ldap);
-        }
-    }
-
-    public List<User> searchUserByName(String name) {
-        LdapContext ldap = null;
-        try {
-            ldap = getLdapContext();
-            return searchUserByName(ldap, name);
-        } catch (Exception e) {
-            LOG.debug(MessageFormat.format("Failed to search user ''{0}''", name), e);
-            return Collections.emptyList();
-        } finally {
-            closeQuietly(ldap);
-        }
-    }
-
-    private User searchUserById(LdapContext ldap, String userId) throws NamingException {
-        SearchControls sc = getSearchControls();
-        NamingEnumeration<SearchResult> results = null;
-        try {
-            results = ldap.search(config.getBaseDN(),
-                MessageFormat.format("(&(objectClass=user)(sAMAccountName={0}))", userId), sc); //$NON-NLS-1$
-            while (results != null && results.hasMore()) {
-                SearchResult entry = results.next();
-                User user = processEntry(entry);
-                if (user != null) {
-                    if (LOG.isDebugEnabled()) {
-                        LOG.debug(MessageFormat.format("Success reading from LDAP: {0}, {1} <{2}>", //$NON-NLS-1$
-                                user.getUserId(), user.getDisplayName(), user.getEmail()));
-                    }
-                    return user;
-                }
-            }
-        } finally {
-            closeQuietly(results);
-        }
-        return new User(userId);
-    }
-
-    private List<User> searchUserByName(LdapContext ldap, String name) throws NamingException {
-        List<User> ret = new ArrayList<User>(0);
-        try {
-            boolean somethingAdded = false;
-            SearchControls sc = getSearchControls();
-            String[] parts = StringUtils.split(NormalizeUtil.normalize(name), " ,"); //$NON-NLS-1$
-            if (parts.length == 1) {
-                somethingAdded = search(parts[0], ret, ldap, sc);
-            }
-            else if (parts.length > 1) {
-                // givenname surname ('Michael Ochmann'), or surname givenname('Ochmann, Michael')
-                NamingEnumeration<SearchResult> results = null;
-                try {
-                    results = ldap.search(
-                        config.getBaseDN(),
-                        MessageFormat.format("(&(objectClass=user)(givenName={0}*)(sn={1}*))", //$NON-NLS-1$
-                                parts[0], parts[1]), sc);
-                    somethingAdded |= addLDAPSearchResult(ret, results);
-                } finally {
-                    closeQuietly(results);
-                }
-                try {
-                    results = ldap.search(
-                            config.getBaseDN(),
-                            MessageFormat.format("(&(objectClass=user)(sn={0}*)(givenName={1}*))", //$NON-NLS-1$
-                                    parts[0], parts[1]), sc);
-                    somethingAdded |= addLDAPSearchResult(ret, results);
-                } finally {
-                    closeQuietly(results);
-                }
-                // givenname initial surname, e.g. 'Michael R. Ochmann'
-                if (parts.length > 2) {
-                    try {
-                        results = ldap.search(
-                                config.getBaseDN(),
-                                MessageFormat.format("(&(objectClass=user)(givenName={0}*)(sn={1}*))", //$NON-NLS-1$
-                                        parts[0], parts[2]), sc);
-                        somethingAdded |= addLDAPSearchResult(ret, results);
-                    } finally {
-                        closeQuietly(results);
-                    }
-                    try {
-                        results = ldap.search(
-                                config.getBaseDN(),
-                                MessageFormat.format("(&(objectClass=user)(sn={0}*)(givenName={1}*))", //$NON-NLS-1$
-                                        parts[0], parts[2]), sc);
-                        somethingAdded |= addLDAPSearchResult(ret, results);
-                    } finally {
-                        closeQuietly(results);
-                    }
-                }
-                if (!somethingAdded) {
-                    // try to match each part individually
-                    for (int i = 0; i < parts.length; ++i) {
-                        somethingAdded = search(parts[i], ret, ldap, sc);
-                    }
-                }
-            }
-        } catch (SizeLimitExceededException e) {
-            // 1000 is good enough at the moment for this use case...
-            LOG.warn(MessageFormat.format("LDAP query size limit exceeded while searching for ''{0}''", name), e);
-        }
-        return ret;
-    }
-
-    private boolean search(String s, List<User> ret, LdapContext ldap, SearchControls sc) throws NamingException {
-        // try a match with surname*
-        boolean somethingAdded = false;
-        NamingEnumeration<SearchResult> results = null;
-        try {
-            results = ldap.search(
-                    config.getBaseDN(),
-                    MessageFormat.format("(&(objectClass=user)(|(sn={0}*)(givenName={1}*)))", s, s), sc); //$NON-NLS-1$
-            somethingAdded = addLDAPSearchResult(ret, results);
-        } finally {
-            closeQuietly(results);
-        }
-        if (!somethingAdded) {
-            try {
-                // try a match with the account name and mail address
-                results = ldap.search(
-                        config.getBaseDN(),
-                        MessageFormat.format("(&(objectClass=user)(sAMAccountName={0}*))", s), sc); //$NON-NLS-1$
-                somethingAdded |= addLDAPSearchResult(ret, results);
-            } finally {
-                closeQuietly(results);
-            }
-            if (!somethingAdded) {
-                try {
-                    // try to match surname~= or givenname~=
-                    results = ldap.search(config.getBaseDN(),
-                            MessageFormat.format("(&(objectClass=user)(|(sn~={0})(givenName~={1})))", s, s), sc); //$NON-NLS-1$
-                    somethingAdded |= addLDAPSearchResult(ret, results);
-                } finally {
-                    closeQuietly(results);
-                }
-                if (!somethingAdded) {
-                    try {
-                        results = ldap.search(config.getBaseDN(),
-                                MessageFormat.format("(&(objectClass=user)(mail={0}*))", s), sc); //$NON-NLS-1$
-                        somethingAdded |= addLDAPSearchResult(ret, results);
-                    } finally {
-                        closeQuietly(results);
-                    }
-                }
-            }
-        }
-        return somethingAdded;
-    }
-
-    // Iterate over a batch of search results sent by the server
-    private boolean addLDAPSearchResult(List<User> users, NamingEnumeration<SearchResult> results)
-            throws NamingException {
-        boolean somethingAdded = false;
-        while (results != null && results.hasMore()) {
-            // Display an entry
-            SearchResult entry = results.next();
-            User user = processEntry(entry);
-            if (user != null) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug(MessageFormat.format("Success reading from LDAP: {0}, {1} <{2}>",
-                            user.getUserId(), user.getDisplayName(), user.getEmail()));
-                }
-                users.add(user);
-                somethingAdded = true;
-            }
-        }
-        return somethingAdded;
-    }
-
-    private String getStringValue(Attributes attributes, LDAPAttributeNames attributeName)
-            throws NamingException {
-        String ret = null;
-        Attribute attribute = attributes.get(attributeName.getLdapKey());
-        if (attribute != null) {
-            for (int i = 0; i < attribute.size(); i++) {
-                ret = (String) attribute.get(i);
-            }
-        }
-        return ret;
-    }
-
-    private User processEntry(SearchResult entry) throws NamingException {
-        User user = new User();
-        Attributes attrs = entry.getAttributes();
-        Attribute attrBits = attrs.get(LDAPAttributeNames.BITS.getLdapKey());
-        if (attrBits != null) {
-            long lng = Long.parseLong(attrBits.get(0).toString());
-            long secondBit = lng & 2; // get bit 2
-            if (secondBit != 0) {
-                // User not enabled
-                return null;
-            }
-        }
-        user.setUserId(StringUtils.lowerCase(getStringValue(attrs, LDAPAttributeNames.USERID)));
-        user.setFirstname(getStringValue(attrs, LDAPAttributeNames.FIRSTNAME));
-        user.setLastname(getStringValue(attrs, LDAPAttributeNames.LASTNAME));
-        user.setEmail(getStringValue(attrs, LDAPAttributeNames.EMAIL));
-        user.setTelephone(getStringValue(attrs, LDAPAttributeNames.TELEPHONE));
-        user.setMobile(getStringValue(attrs, LDAPAttributeNames.MOBILE));
-        user.setRoom(getStringValue(attrs, LDAPAttributeNames.ROOM));
-        user.setLocation(getStringValue(attrs, LDAPAttributeNames.LOCATION));
-        user.setDepartment(getStringValue(attrs, LDAPAttributeNames.DEPARTMENT));
-        user.setCompany(getStringValue(attrs, LDAPAttributeNames.COMPANY));
-        user.setSip(getStringValue(attrs, LDAPAttributeNames.SIP));
-        return user;
-    }
-
-    @SuppressWarnings("nls")
-    private SearchControls getSearchControls() {
-        SearchControls sc = new SearchControls();
-        if ("base".equalsIgnoreCase(config.getSearchScope())) {
-            sc.setSearchScope(SearchControls.OBJECT_SCOPE);
-        } else if ("onelevel".equalsIgnoreCase(config.getSearchScope())) {
-            sc.setSearchScope(SearchControls.ONELEVEL_SCOPE);
-        } else if ("subtree".equalsIgnoreCase(config.getSearchScope())) {
-            sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
-        }
-        sc.setReturningAttributes(LDAPAttributeNames.getAll());
-        return sc;
-    }
-}
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPConfig.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPConfig.java
index b657e98..1706019 100644
--- a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPConfig.java
+++ b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPConfig.java
@@ -10,62 +10,47 @@
  *******************************************************************************/
 package org.eclipse.skalli.core.user.ldap;
 
-import org.eclipse.skalli.services.configuration.Protect;
-
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 
 @XStreamAlias("ldap")
 public class LDAPConfig {
 
-    @Protect
-    private String password;
-    @Protect
-    private String username;
-    private String authentication;
-    private String referral;
-    private String providerUrl;
-    private String ctxFactory;
-    private boolean sslNoVerify;
-
+    private String providerId;
+    private String destination;
     private String baseDN;
     private String searchScope;
-
     private String cacheSize;
 
     // do not remove: required by xstream
     public LDAPConfig() {
     }
 
-    public String getPassword() {
-        return password;
+    /**
+     * @return the providerId
+     */
+    public String getProviderId() {
+        return providerId;
     }
 
-    public void setPassword(String password) {
-        this.password = password;
+    /**
+     * @param providerId the providerId to set
+     */
+    public void setProviderId(String providerId) {
+        this.providerId = providerId;
     }
 
-    public String getUsername() {
-        return username;
+    /**
+     * @return the destination
+     */
+    public String getDestination() {
+        return destination;
     }
 
-    public void setUsername(String username) {
-        this.username = username;
-    }
-
-    public String getProviderUrl() {
-        return providerUrl;
-    }
-
-    public void setProviderUrl(String providerUrl) {
-        this.providerUrl = providerUrl;
-    }
-
-    public String getCtxFactory() {
-        return ctxFactory;
-    }
-
-    public void setCtxFactory(String ctxFactory) {
-        this.ctxFactory = ctxFactory;
+    /**
+     * @param destination the destination to set
+     */
+    public void setDestination(String destination) {
+        this.destination = destination;
     }
 
     public String getBaseDN() {
@@ -85,48 +70,6 @@
     }
 
     /**
-     * @return the authentication
-     */
-    public String getAuthentication() {
-        return authentication;
-    }
-
-    /**
-     * @param authentication the authentication to set
-     */
-    public void setAuthentication(String authentication) {
-        this.authentication = authentication;
-    }
-
-    /**
-     * @return the referral
-     */
-    public String getReferral() {
-        return referral;
-    }
-
-    /**
-     * @param referral the referral to set
-     */
-    public void setReferral(String referral) {
-        this.referral = referral;
-    }
-
-    /**
-     * @return the sslVerify
-     */
-    public boolean isSslNoVerify() {
-        return sslNoVerify;
-    }
-
-    /**
-     * @param sslVerify the sslVerify to set
-     */
-    public void setSslNoVerify(boolean sslNoVerify) {
-        this.sslNoVerify = sslNoVerify;
-    }
-
-    /**
      * Returns the desired search scope.
      *
      * @return either "base" or "onelevel", or "subtree".
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPUserComponent.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPUserComponent.java
index 49a1c6e..63d46cd 100644
--- a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPUserComponent.java
+++ b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/user/ldap/LDAPUserComponent.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010-2014 SAP AG and others.
+ * Copyright (c) 2010-2016 SAP AG and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
@@ -11,23 +11,39 @@
 package org.eclipse.skalli.core.user.ldap;
 
 import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.SizeLimitExceededException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapContext;
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.math.NumberUtils;
+import org.eclipse.skalli.commons.CollectionUtils;
+import org.eclipse.skalli.commons.VisibleForTesting;
 import org.eclipse.skalli.core.cache.Cache;
 import org.eclipse.skalli.core.cache.GroundhogCache;
+import org.eclipse.skalli.core.user.NormalizeUtil;
 import org.eclipse.skalli.model.User;
 import org.eclipse.skalli.services.configuration.ConfigurationService;
+import org.eclipse.skalli.services.configuration.Configurations;
 import org.eclipse.skalli.services.configuration.EventConfigUpdate;
 import org.eclipse.skalli.services.event.EventListener;
 import org.eclipse.skalli.services.event.EventService;
 import org.eclipse.skalli.services.user.UserService;
+import org.eclipse.skalli.services.user.ldap.LdapContextProvider;
 import org.osgi.service.component.ComponentConstants;
 import org.osgi.service.component.ComponentContext;
 import org.slf4j.Logger;
@@ -35,25 +51,45 @@
 
 /**
  * Implementation of {@link UserService} accessing an LDAP server.
- * It relies on the {@link ConfigurationService} for LDAP authentication
- * and location information.
  */
 public class LDAPUserComponent implements UserService, EventListener<EventConfigUpdate> {
 
     private static final Logger LOG = LoggerFactory.getLogger(LDAPUserComponent.class);
 
+    private static final String DEFAULT_POOL_PROTOCOLS = "plain ssl"; //$NON-NLS-1$
+    private static final int DEFAULT_POOL_MAXSIZE = 5;
+    private static final long DEFAULT_POOL_TIMEOUT = 10000L;
     private static final int DEFAULT_CACHE_SIZE = 100;
 
+    private static final String CONNECT_POOL_PROTOCOLS = "com.sun.jndi.ldap.connect.pool.protocol"; //$NON-NLS-1$
+    private static final String CONNECT_POOL_DEBUG = "com.sun.jndi.ldap.connect.pool.debug"; //$NON-NLS-1$
+    private static final String CONNECT_POOL_MAXSIZE = "com.sun.jndi.ldap.connect.pool.maxsize"; //$NON-NLS-1$
+    private static final String CONNECT_POOL_TIMEOUT = "com.sun.jndi.ldap.connect.pool.timeout"; //$NON-NLS-1$
+
+    private ConcurrentHashMap<String,LdapContextProvider> ctxProviders =
+            new ConcurrentHashMap<String, LdapContextProvider>();
+
+    private LdapContextProvider ctxProvider;
+    private String destination;
+    private String baseDN;
+    private String searchScope;
+
     private Cache<String, User> cache;
 
-    private EventService eventService;
-    private ConfigurationService configurationService;
-
     protected void activate(ComponentContext context) {
+        // define properties for the LDAP connection pool globally, but let the individual context providers
+        // decide whether to use the connection pool or not; note, for some weird reason the pool properties
+        // are system properties and cannot be set per context
+        System.setProperty(CONNECT_POOL_PROTOCOLS, DEFAULT_POOL_PROTOCOLS);
+        System.setProperty(CONNECT_POOL_MAXSIZE, Integer.toString(DEFAULT_POOL_MAXSIZE));
+        System.setProperty(CONNECT_POOL_TIMEOUT, Long.toString(DEFAULT_POOL_TIMEOUT));
+        if (LOG.isDebugEnabled()) {
+            System.setProperty(CONNECT_POOL_DEBUG, "fine"); //$NON-NLS-1$
+        }
+
         LOG.info(MessageFormat.format("[UserService][LDAP] {0} : activated", //$NON-NLS-1$
                 (String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
-        eventService.registerListener(EventConfigUpdate.class, this);
-        initializeCache();
+        initialize();
     }
 
     protected void deactivate(ComponentContext context) {
@@ -62,43 +98,46 @@
     }
 
     protected void bindEventService(EventService eventService) {
-        this.eventService = eventService;
+        eventService.registerListener(EventConfigUpdate.class, this);
     }
 
     protected void unbindEventService(EventService eventService) {
-        this.eventService = null;
+        eventService.unregisterListener(EventConfigUpdate.class, this);
     }
 
     protected void bindConfigurationService(ConfigurationService configurationService) {
-        this.configurationService = configurationService;
-        initializeCache();
+        initialize();
     }
 
     protected void unbindConfigurationService(ConfigurationService configurationService) {
-        this.configurationService = null;
     }
 
-    private synchronized void initializeCache() {
+    protected void bindLdapContextProvider(LdapContextProvider ctxProvider) {
+        ctxProviders.put(ctxProvider.getId(), ctxProvider);
+        initialize();
+    }
+
+    protected void unbindLdapContextProvider(LdapContextProvider ctxProvider) {
+        ctxProviders.remove(ctxProvider.getId());
+    }
+
+    @VisibleForTesting
+    protected synchronized void initialize() {
         int cacheSize = DEFAULT_CACHE_SIZE;
-        if (configurationService != null) {
-            LDAPConfig config = configurationService.readConfiguration(LDAPConfig.class);
-            if (config != null) {
-                cacheSize = NumberUtils.toInt(config.getCacheSize(), DEFAULT_CACHE_SIZE);
-            }
+        LDAPConfig ldapConfig = Configurations.getConfiguration(LDAPConfig.class);
+        if (ldapConfig != null) {
+            String providerId = StringUtils.isNotBlank(ldapConfig.getProviderId())
+                    ? ldapConfig.getProviderId()
+                    : "default"; //$NON-NLS-1$
+            ctxProvider = ctxProviders.get(providerId);
+            destination = ldapConfig.getDestination();
+            baseDN = ldapConfig.getBaseDN();
+            searchScope = ldapConfig.getSearchScope();
+            cacheSize = NumberUtils.toInt(ldapConfig.getCacheSize(), DEFAULT_CACHE_SIZE);
         }
         cache = new GroundhogCache<String, User>(cacheSize, cache);
     }
 
-    private LDAPClient getLDAPClient() {
-        if (configurationService != null) {
-            LDAPConfig config = configurationService.readConfiguration(LDAPConfig.class);
-            if (config != null) {
-                return new LDAPClient(config);
-            }
-        }
-        return null;
-    }
-
     @Override
     public String getType() {
         return "ldap"; //$NON-NLS-1$
@@ -109,11 +148,10 @@
         if (StringUtils.isBlank(searchText)) {
             return Collections.emptyList();
         }
-        LDAPClient ldap = getLDAPClient();
-        if (ldap == null) {
+        if (ctxProvider == null) {
             return Collections.emptyList();
         }
-        List<User> users = ldap.searchUserByName(searchText);
+        List<User> users = searchUserByName(searchText);
         for (User user : users) {
             if (user != null) {
                 cache.put(StringUtils.lowerCase(user.getUserId()), user);
@@ -130,11 +168,10 @@
         String lowerCaseUserId = userId.toLowerCase(Locale.ENGLISH);
         User user = cache.get(lowerCaseUserId);
         if (user == null) {
-            LDAPClient ldap = getLDAPClient();
-            if (ldap == null) {
+            if (ctxProvider == null) {
                 return null;
             }
-            user = ldap.searchUserById(userId);
+            user = searchUserById(userId);
             if (user != null) {
                 cache.put(lowerCaseUserId, user);
             }
@@ -148,7 +185,7 @@
     }
 
     @Override
-    public Set<User> getUsersById(Set<String> userIds) {
+    public synchronized Set<User> getUsersById(Set<String> userIds) {
         if (userIds == null || userIds.isEmpty()) {
             return Collections.emptySet();
         }
@@ -167,11 +204,10 @@
             }
         }
         if (notFoundInCache.size() > 0) {
-            LDAPClient ldap = getLDAPClient();
-            if (ldap == null) {
+            if (ctxProvider == null) {
                 return users;
             }
-            Set<User> ldapUsers = ldap.searchUsersByIds(notFoundInCache);
+            Set<User> ldapUsers = searchUsersByIds(notFoundInCache);
             for (User user : ldapUsers) {
                 if (user != null) {
                     String userId = user.getUserId();
@@ -188,8 +224,276 @@
     @Override
     public synchronized void onEvent(EventConfigUpdate event) {
         if (LDAPConfig.class.equals(event.getConfigClass())) {
-            initializeCache();
+            initialize();
         }
     }
 
+    private User searchUserById(String userId) {
+        LdapContext ctx = null;
+        try {
+            ctx = ctxProvider.getLdapContext(destination);
+            return searchUserById(ctx, userId);
+        }  catch (Exception e) {
+            LOG.debug(MessageFormat.format("Failed to retrieve user ''{0}''", userId), e);
+            return new User(userId);
+        } finally {
+            closeQuietly(ctx);
+        }
+    }
+
+    private Set<User> searchUsersByIds(Set<String> userIds) {
+        LdapContext ctx = null;
+        try {
+            if (ctxProvider == null) {
+                return Collections.emptySet();
+            }
+            ctx = ctxProvider.getLdapContext(destination);
+            Set<User> ret = new HashSet<User>();
+            for (String userId : userIds) {
+                ret.add(searchUserById(ctx, userId));
+            }
+            return ret;
+        } catch (Exception e) {
+            LOG.debug(MessageFormat.format("Failed to retrieve users {0}",
+                    CollectionUtils.toString(userIds, ',')), e);
+            return Collections.emptySet();
+        } finally {
+            closeQuietly(ctx);
+        }
+    }
+
+    private List<User> searchUserByName(String name) {
+        LdapContext ctx = null;
+        try {
+            if (ctxProvider == null) {
+                return Collections.emptyList();
+            }
+            ctx = ctxProvider.getLdapContext(destination);
+            return searchUserByName(ctx, name);
+        } catch (Exception e) {
+            LOG.debug(MessageFormat.format("Failed to search user ''{0}''", name), e);
+            return Collections.emptyList();
+        } finally {
+            closeQuietly(ctx);
+        }
+    }
+
+    private User searchUserById(LdapContext ctx, String userId) throws NamingException {
+        SearchControls sc = getSearchControls();
+        NamingEnumeration<SearchResult> results = null;
+        try {
+            results = ctx.search(baseDN,
+                MessageFormat.format("(&(objectClass=user)(sAMAccountName={0}))", userId), sc); //$NON-NLS-1$
+            while (results != null && results.hasMore()) {
+                SearchResult entry = results.next();
+                User user = processEntry(entry);
+                if (user != null) {
+                    if (LOG.isDebugEnabled()) {
+                        LOG.debug(MessageFormat.format("Success reading from LDAP: {0}, {1} <{2}>", //$NON-NLS-1$
+                                user.getUserId(), user.getDisplayName(), user.getEmail()));
+                    }
+                    return user;
+                }
+            }
+        } finally {
+            closeQuietly(results);
+        }
+        return new User(userId);
+    }
+
+    private List<User> searchUserByName(LdapContext ctx, String name) throws NamingException {
+        List<User> ret = new ArrayList<User>(0);
+        try {
+            boolean somethingAdded = false;
+            SearchControls sc = getSearchControls();
+            String[] parts = StringUtils.split(NormalizeUtil.normalize(name), " ,"); //$NON-NLS-1$
+            if (parts.length == 1) {
+                somethingAdded = search(parts[0], ret, ctx, sc);
+            }
+            else if (parts.length > 1) {
+                // givenname surname ('Michael Ochmann'), or surname givenname('Ochmann, Michael')
+                NamingEnumeration<SearchResult> results = null;
+                try {
+                    results = ctx.search(baseDN,
+                        MessageFormat.format("(&(objectClass=user)(givenName={0}*)(sn={1}*))", //$NON-NLS-1$
+                                parts[0], parts[1]), sc);
+                    somethingAdded |= addLDAPSearchResult(ret, results);
+                } finally {
+                    closeQuietly(results);
+                }
+                try {
+                    results = ctx.search(baseDN,
+                            MessageFormat.format("(&(objectClass=user)(sn={0}*)(givenName={1}*))", //$NON-NLS-1$
+                                    parts[0], parts[1]), sc);
+                    somethingAdded |= addLDAPSearchResult(ret, results);
+                } finally {
+                    closeQuietly(results);
+                }
+                // givenname initial surname, e.g. 'Michael R. Ochmann'
+                if (parts.length > 2) {
+                    try {
+                        results = ctx.search(baseDN,
+                                MessageFormat.format("(&(objectClass=user)(givenName={0}*)(sn={1}*))", //$NON-NLS-1$
+                                        parts[0], parts[2]), sc);
+                        somethingAdded |= addLDAPSearchResult(ret, results);
+                    } finally {
+                        closeQuietly(results);
+                    }
+                    try {
+                        results = ctx.search(baseDN,
+                                MessageFormat.format("(&(objectClass=user)(sn={0}*)(givenName={1}*))", //$NON-NLS-1$
+                                        parts[0], parts[2]), sc);
+                        somethingAdded |= addLDAPSearchResult(ret, results);
+                    } finally {
+                        closeQuietly(results);
+                    }
+                }
+                if (!somethingAdded) {
+                    // try to match each part individually
+                    for (int i = 0; i < parts.length; ++i) {
+                        somethingAdded = search(parts[i], ret, ctx, sc);
+                    }
+                }
+            }
+        } catch (SizeLimitExceededException e) {
+            // 1000 is good enough at the moment for this use case...
+            LOG.warn(MessageFormat.format("LDAP query size limit exceeded while searching for ''{0}''", name), e);
+        }
+        return ret;
+    }
+
+    private boolean search(String s, List<User> ret, LdapContext ctx, SearchControls sc) throws NamingException {
+        // try a match with surname*
+        boolean somethingAdded = false;
+        NamingEnumeration<SearchResult> results = null;
+        try {
+            results = ctx.search(baseDN,
+                    MessageFormat.format("(&(objectClass=user)(|(sn={0}*)(givenName={1}*)))", s, s), sc); //$NON-NLS-1$
+            somethingAdded = addLDAPSearchResult(ret, results);
+        } finally {
+            closeQuietly(results);
+        }
+        if (!somethingAdded) {
+            try {
+                // try a match with the account name and mail address
+                results = ctx.search(baseDN,
+                        MessageFormat.format("(&(objectClass=user)(sAMAccountName={0}*))", s), sc); //$NON-NLS-1$
+                somethingAdded |= addLDAPSearchResult(ret, results);
+            } finally {
+                closeQuietly(results);
+            }
+            if (!somethingAdded) {
+                try {
+                    // try to match surname~= or givenname~=
+                    results = ctx.search(baseDN,
+                            MessageFormat.format("(&(objectClass=user)(|(sn~={0})(givenName~={1})))", s, s), sc); //$NON-NLS-1$
+                    somethingAdded |= addLDAPSearchResult(ret, results);
+                } finally {
+                    closeQuietly(results);
+                }
+                if (!somethingAdded) {
+                    try {
+                        results = ctx.search(baseDN,
+                                MessageFormat.format("(&(objectClass=user)(mail={0}*))", s), sc); //$NON-NLS-1$
+                        somethingAdded |= addLDAPSearchResult(ret, results);
+                    } finally {
+                        closeQuietly(results);
+                    }
+                }
+            }
+        }
+        return somethingAdded;
+    }
+
+    // Iterate over a batch of search results sent by the server
+    private boolean addLDAPSearchResult(List<User> users, NamingEnumeration<SearchResult> results)
+            throws NamingException {
+        boolean somethingAdded = false;
+        while (results != null && results.hasMore()) {
+            // Display an entry
+            SearchResult entry = results.next();
+            User user = processEntry(entry);
+            if (user != null) {
+                if (LOG.isDebugEnabled()) {
+                    LOG.debug(MessageFormat.format("Success reading from LDAP: {0}, {1} <{2}>",
+                            user.getUserId(), user.getDisplayName(), user.getEmail()));
+                }
+                users.add(user);
+                somethingAdded = true;
+            }
+        }
+        return somethingAdded;
+    }
+
+    private String getStringValue(Attributes attributes, LDAPAttributeNames attributeName)
+            throws NamingException {
+        String ret = null;
+        Attribute attribute = attributes.get(attributeName.getLdapKey());
+        if (attribute != null) {
+            for (int i = 0; i < attribute.size(); i++) {
+                ret = (String) attribute.get(i);
+            }
+        }
+        return ret;
+    }
+
+    private User processEntry(SearchResult entry) throws NamingException {
+        User user = new User();
+        Attributes attrs = entry.getAttributes();
+        Attribute attrBits = attrs.get(LDAPAttributeNames.BITS.getLdapKey());
+        if (attrBits != null) {
+            long lng = Long.parseLong(attrBits.get(0).toString());
+            long secondBit = lng & 2; // get bit 2
+            if (secondBit != 0) {
+                // User not enabled
+                return null;
+            }
+        }
+        user.setUserId(StringUtils.lowerCase(getStringValue(attrs, LDAPAttributeNames.USERID)));
+        user.setFirstname(getStringValue(attrs, LDAPAttributeNames.FIRSTNAME));
+        user.setLastname(getStringValue(attrs, LDAPAttributeNames.LASTNAME));
+        user.setEmail(getStringValue(attrs, LDAPAttributeNames.EMAIL));
+        user.setTelephone(getStringValue(attrs, LDAPAttributeNames.TELEPHONE));
+        user.setMobile(getStringValue(attrs, LDAPAttributeNames.MOBILE));
+        user.setRoom(getStringValue(attrs, LDAPAttributeNames.ROOM));
+        user.setLocation(getStringValue(attrs, LDAPAttributeNames.LOCATION));
+        user.setDepartment(getStringValue(attrs, LDAPAttributeNames.DEPARTMENT));
+        user.setCompany(getStringValue(attrs, LDAPAttributeNames.COMPANY));
+        user.setSip(getStringValue(attrs, LDAPAttributeNames.SIP));
+        return user;
+    }
+
+    @SuppressWarnings("nls")
+    private SearchControls getSearchControls() {
+        SearchControls sc = new SearchControls();
+        if ("base".equalsIgnoreCase(searchScope)) {
+            sc.setSearchScope(SearchControls.OBJECT_SCOPE);
+        } else if ("onelevel".equalsIgnoreCase(searchScope)) {
+            sc.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+        } else if ("subtree".equalsIgnoreCase(searchScope)) {
+            sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
+        }
+        sc.setReturningAttributes(LDAPAttributeNames.getAll());
+        return sc;
+    }
+
+    private void closeQuietly(LdapContext ctx) {
+        if (ctx != null) {
+            try {
+                ctx .close();
+            } catch (NamingException e) {
+                LOG.error("Failed to close LDAP connection", e);
+            }
+        }
+    }
+
+    private void closeQuietly(NamingEnumeration<?> result) {
+        if (result != null) {
+            try {
+                result.close();
+            } catch (NamingException e) {
+                LOG.error("Failed to close LDAP result set", e);
+            }
+        }
+    }
 }