001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2015 ForgeRock AS. 015 */ 016package org.forgerock.openig.ldap; 017 018import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool; 019import static org.forgerock.opendj.ldap.LDAPConnectionFactory.HEARTBEAT_ENABLED; 020import static org.forgerock.util.Options.defaultOptions; 021 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Map; 025import java.util.concurrent.ConcurrentHashMap; 026 027import org.forgerock.opendj.ldap.ConnectionFactory; 028import org.forgerock.opendj.ldap.DN; 029import org.forgerock.opendj.ldap.Filter; 030import org.forgerock.opendj.ldap.LDAPConnectionFactory; 031import org.forgerock.opendj.ldap.LdapException; 032import org.forgerock.opendj.ldap.SearchScope; 033import org.forgerock.services.TransactionId; 034import org.forgerock.services.context.Context; 035import org.forgerock.services.context.TransactionIdContext; 036import org.forgerock.util.Option; 037import org.forgerock.util.Options; 038 039/** 040 * This class acts as a simplified access point into the OpenDJ LDAP SDK. Whilst 041 * it is possible for scripts to access the OpenDJ LDAP SDK APIs directly, this 042 * class simplifies the most common use cases by exposes fields and methods for: 043 * <ul> 044 * <li>creating and caching LDAP connections 045 * <li>parsing DNs and LDAP filters 046 * <li>simple access to LDAP scopes. 047 * </ul> 048 */ 049public final class LdapClient { 050 051 /** 052 * The option to pass the TransactionId to LdapConnection. 053 */ 054 public static final Option<TransactionId> TRANSACTIONID_OPTION = Option.of(TransactionId.class, null); 055 056 private static final LdapClient INSTANCE = new LdapClient(); 057 058 /** 059 * Returns an instance of an {@code LdapClient}. 060 * 061 * @return An instance of an {@code LdapClient}. 062 */ 063 public static LdapClient getInstance() { 064 return INSTANCE; 065 } 066 067 /** 068 * Setup the default options to create a LdapClient and adds the transactionId if any in the context's chain. 069 * @param context the context's chain 070 * @return the default options with the current transactionId set if any in the context's chain 071 */ 072 public static Options defaultOptions(Context context) { 073 Options defaultOptions = Options.defaultOptions(); 074 075 if (context.containsContext(TransactionIdContext.class)) { 076 TransactionIdContext txContext = context.asContext(TransactionIdContext.class); 077 defaultOptions.set(TRANSACTIONID_OPTION, txContext.getTransactionId()); 078 } 079 080 return defaultOptions; 081 } 082 083 084 private final ConcurrentHashMap<String, ConnectionFactory> factories = 085 new ConcurrentHashMap<>(); 086 087 /** 088 * A map containing the LDAP scopes making it easier to specify scopes 089 * within scripts where Maps are exposed as properties, e.g. in Groovy the 090 * sub-tree scope may be specified using the value "ldap.scope.sub". 091 */ 092 private final Map<String, SearchScope> scope; 093 094 private LdapClient() { 095 final Map<String, SearchScope> map = new HashMap<>(4); 096 for (final SearchScope scope : SearchScope.values()) { 097 map.put(scope.toString(), scope); 098 } 099 scope = Collections.unmodifiableMap(map); 100 } 101 102 /** 103 * Returns the {@link SearchScope} available. 104 * @return the {@link SearchScope} available. 105 */ 106 public Map<String, SearchScope> getScope() { 107 return scope; 108 } 109 110 /** 111 * Returns an LDAP connection for the specified LDAP server. The returned 112 * connection must be closed once the caller has completed its transaction. 113 * Connections are cached between calls using a connection pool. 114 * 115 * @param host The LDAP server host name. 116 * @param port The LDAP server port. 117 * @return An LDAP connection for the specified LDAP server. 118 * @throws LdapException If an error occurred while connecting to the LDAP server. 119 */ 120 public LdapConnection connect(final String host, final int port) throws LdapException { 121 return connect(host, port, Options.defaultOptions()); 122 } 123 124 /** 125 * Returns an LDAP connection for the specified LDAP server using the 126 * provided LDAP options. The returned connection must be closed once the 127 * caller has completed its transaction. Connections are cached between 128 * calls using a connection pool. The LDAP options may be used for 129 * configuring SSL parameters and timeouts. 130 * <p> 131 * NOTE: if a connection has already been obtained to the specified LDAP 132 * server then a cached connection will be returned and the LDAP options 133 * will be ignored. 134 * 135 * @param host The LDAP server host name. 136 * @param port The LDAP server port. 137 * @param options The LDAP options. 138 * @return An LDAP connection for the specified LDAP server. 139 * @throws LdapException If an error occurred while connecting to the LDAP server. 140 */ 141 public LdapConnection connect(final String host, final int port, final Options options) 142 throws LdapException { 143 final ConnectionFactory factory = getConnectionFactory(host, port, options); 144 return new LdapConnection(factory.getConnection(), options.get(TRANSACTIONID_OPTION)); 145 } 146 147 /** 148 * Formats an LDAP distinguished name using the provided template and 149 * attribute values. Values will be safely escaped in order to avoid 150 * potential injection attacks. 151 * 152 * @param template The DN template. 153 * @param attributeValues The attribute values to be substituted into the template. 154 * @return The formatted template parsed as a {@code DN}. 155 * @throws org.forgerock.i18n.LocalizedIllegalArgumentException If the formatted template is not a valid LDAP string 156 * representation of a DN. 157 * @see DN#format(String, Object...) 158 */ 159 public String dn(final String template, final Object... attributeValues) { 160 return DN.format(template, attributeValues).toString(); 161 } 162 163 /** 164 * Formats an LDAP filter using the provided template and assertion values. 165 * Values will be safely escaped in order to avoid potential injection 166 * attacks. 167 * 168 * @param template The filter template. 169 * @param assertionValues The assertion values to be substituted into the template. 170 * @return The formatted template parsed as a {@code Filter}. 171 * @throws org.forgerock.i18n.LocalizedIllegalArgumentException If the formatted template is not a valid LDAP string 172 * representation of a filter. 173 * @see Filter#format(String, Object...) 174 */ 175 public String filter(final String template, final Object... assertionValues) { 176 return Filter.format(template, assertionValues).toString(); 177 } 178 179 private ConnectionFactory getConnectionFactory(final String host, final int port, 180 final Options options) { 181 final String key = host + ":" + port; 182 ConnectionFactory factory = factories.get(key); 183 if (factory == null) { 184 synchronized (factories) { 185 factory = factories.get(key); 186 if (factory == null) { 187 options.set(HEARTBEAT_ENABLED, true); 188 factory = newCachedConnectionPool(new LDAPConnectionFactory(host, port, options)); 189 factories.put(key, factory); 190 } 191 } 192 } 193 return factory; 194 } 195 196 @Override 197 protected void finalize() throws Throwable { 198 for (ConnectionFactory factory : factories.values()) { 199 factory.close(); 200 } 201 factories.clear(); 202 super.finalize(); 203 } 204}