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}