/******************************************************************************* * Copyright 2014 Miami-Dade County * * Licensed 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.sharegov.cirm.user; import java.util.Properties; import javax.naming.*; import javax.naming.directory.*; import javax.naming.ldap.*; import org.sharegov.cirm.utils.GenUtils; /** * Thread safe, self recovering Ldap client with limited functionality * * @author Thomas Hilpold * */ public class LDAPClient { final ThreadLocal<LdapContext> ldapContextsByThread = new ThreadLocal<LdapContext>(); private final Properties params; private volatile InitialLdapContext ctx; private static abstract class Operation { final LdapContext threadCtx; Operation(final LdapContext threadCtx) { this.threadCtx = threadCtx; } final Object exec() throws NamingException { return run(); } abstract Object run() throws NamingException; LdapContext getCtx() { return threadCtx; } @Override protected void finalize() throws Throwable { super.finalize(); try { if (threadCtx != null) threadCtx.close(); } catch (Exception e) { System.err.println("LDAP ERROR ON INITIAL CONTEXT CLOSE " + e); } } } public LDAPClient(Properties params) { if (params == null) throw new RuntimeException("LDAPClient created with null params."); this.params = params; init(); } private LdapContext getContextForThread() { LdapContext threadContext = ldapContextsByThread.get(); if (threadContext == null) { try { synchronized(this) { if (ctx == null) init(); if (ctx == null) throw new RuntimeException("LDAP down, we'll recover once it's back up."); System.out.println("LDAP getting newInstance of context for" + Thread.currentThread()); threadContext = ctx.newInstance(null); } ldapContextsByThread.set(threadContext); } catch (NamingException e) { System.err.println("LDAP Failed to create context for thread: " + Thread.currentThread()); ctx = null; GenUtils.rethrowRuntime(e); } } return threadContext; } private synchronized void init() { try { ctx = new InitialLdapContext(params, null); } catch (NamingException e) { System.err.println("LDAP Could not initialize initial context with parameters: " + params); e.printStackTrace(); ctx = null; } } private Object exec(Operation op) { boolean reconnected = false; try { do { try { return op.exec(); } catch(CommunicationException e) { if (reconnected) throw e; System.out.println("LDAP reconnecting op ctx " + Thread.currentThread().getName()); op.getCtx().reconnect(null); reconnected = true; } } while (reconnected); } catch (NamingException e) { System.err.println("LDAP ERROR :" + e + " on " + Thread.currentThread()); //clear threads context try { op.getCtx().close(); } catch (Exception e2) { System.err.println("LDAP close failed " + e2); } ldapContextsByThread.remove(); GenUtils.rethrowRuntime(e); } return null; // unreachable } /** * <p> * Perform a search in LDAP and return the results. Several parameters control the * results returned. The search is by default recursive and will take as long as needed * to finish (no timeout). All attributes are returned in the results. To select only * certain attributes to be returned, use one of the overloaded methods. * </p> * * @param baseName The starting DN. * @param ldapFilter The LDAP entry filter. * @param pageSize The number of entries per returned page. Use 0 for no paging. * @param sortBy The attribute by which returned entries are sorted. * @return an <code>Enumeration</code> of <code>javax.naming.directory.SearchResult</code> * instances */ public NamingEnumeration<?> search(final String baseName, final String ldapFilter, final String [] sortBy) { try { if (baseName == null) throw new NullPointerException("LDAPClient.search: null base name."); final boolean sorting = (sortBy != null && sortBy.length > 0); final Control [] requestControls; if (sorting) { requestControls = new Control[] { new SortControl(sortBy, Control.NONCRITICAL) }; } else requestControls = null; final String filter = ldapFilter == null ? "" : ldapFilter; return (NamingEnumeration<?>)exec(new Operation(getContextForThread()) { public Object run() throws NamingException { LdapContext threadCtx = getCtx(); threadCtx.setRequestControls(null); if (requestControls != null) { threadCtx.setRequestControls(requestControls); } return threadCtx.search(baseName, filter, new SearchControls(SearchControls.SUBTREE_SCOPE, 0, 0, null, true, true)); } }); } catch (Throwable t) { System.err.println("Could not perform LDAP search starting at " + baseName + " and filter " + ldapFilter); GenUtils.rethrowRuntime(t); return null; // unreachable } } public Object get(final String name) { try { return exec(new Operation(getContextForThread()) { public Object run() throws NamingException { return getCtx().lookup(name); }}); } catch (Throwable t) { error("Failed to retrieve name '" + name + "'", t); return null; } } private void error(String msg, Throwable t) { System.err.println("LDAP Access Error: " + msg + "; stack trace may follow."); if (t != null) t.printStackTrace(System.err); } @Override protected void finalize() throws Throwable { super.finalize(); try { if (ctx != null) ctx.close(); } catch (Exception e) { System.err.println("LDAP ERROR ON INITIAL CONTEXT CLOSE " + e); } } }