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 2016 ForgeRock AS.
015 *
016 */
017package org.forgerock.opendj.rest2ldap;
018
019import static java.util.Arrays.asList;
020import static java.util.Collections.emptyList;
021import static org.forgerock.http.routing.RouteMatchers.newResourceApiVersionBehaviourManager;
022import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
023import static org.forgerock.http.routing.Version.version;
024import static org.forgerock.http.util.Json.readJsonLenient;
025import static org.forgerock.json.JsonValueFunctions.enumConstant;
026import static org.forgerock.json.JsonValueFunctions.pointer;
027import static org.forgerock.json.JsonValueFunctions.setOf;
028import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
029import static org.forgerock.json.resource.RouteMatchers.resourceApiVersionContextFilter;
030import static org.forgerock.opendj.ldap.Connections.LOAD_BALANCER_MONITORING_INTERVAL;
031import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
032import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer;
033import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
034import static org.forgerock.opendj.ldap.KeyManagers.useJvmDefaultKeyStore;
035import static org.forgerock.opendj.ldap.KeyManagers.useKeyStoreFile;
036import static org.forgerock.opendj.ldap.KeyManagers.usePKCS11Token;
037import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
038import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
039import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore;
040import static org.forgerock.opendj.ldap.TrustManagers.trustAll;
041import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
042import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
043import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
044import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException;
045import static org.forgerock.util.Utils.joinAsString;
046import static org.forgerock.util.time.Duration.duration;
047
048import java.io.BufferedReader;
049import java.io.File;
050import java.io.FileFilter;
051import java.io.FileInputStream;
052import java.io.FileReader;
053import java.io.IOException;
054import java.io.InputStream;
055import java.security.GeneralSecurityException;
056import java.util.ArrayList;
057import java.util.Collections;
058import java.util.LinkedHashMap;
059import java.util.LinkedList;
060import java.util.List;
061import java.util.Map;
062import java.util.concurrent.TimeUnit;
063
064import javax.net.ssl.TrustManager;
065import javax.net.ssl.X509KeyManager;
066
067import org.forgerock.http.routing.ResourceApiVersionBehaviourManager;
068import org.forgerock.i18n.LocalizedIllegalArgumentException;
069import org.forgerock.i18n.slf4j.LocalizedLogger;
070import org.forgerock.json.JsonValue;
071import org.forgerock.json.resource.BadRequestException;
072import org.forgerock.json.resource.FilterChain;
073import org.forgerock.json.resource.Request;
074import org.forgerock.json.resource.RequestHandler;
075import org.forgerock.json.resource.ResourceException;
076import org.forgerock.json.resource.Router;
077import org.forgerock.opendj.ldap.ConnectionFactory;
078import org.forgerock.opendj.ldap.LDAPConnectionFactory;
079import org.forgerock.opendj.ldap.SSLContextBuilder;
080import org.forgerock.opendj.ldap.requests.BindRequest;
081import org.forgerock.opendj.ldap.requests.Requests;
082import org.forgerock.services.context.Context;
083import org.forgerock.util.Options;
084import org.forgerock.util.promise.Promise;
085import org.forgerock.util.time.Duration;
086
087/** Provides core factory methods and builders for constructing Rest2Ldap endpoints from JSON configuration. */
088public final class Rest2LdapJsonConfigurator {
089    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
090
091    /**
092     * Parses Rest2Ldap configuration options. The JSON configuration must have the following format:
093     * <p>
094     * <pre>
095     * {
096     *      "readOnUpdatePolicy": "controls",
097     *      "useSubtreeDelete": true,
098     *      "usePermissiveModify": true,
099     *      "useMvcc": true
100     *      "mvccAttribute": "etag"
101     * }
102     * </pre>
103     * <p>
104     * See the sample configuration file for a detailed description of its content.
105     *
106     * @param config
107     *         The JSON configuration.
108     * @return The parsed Rest2Ldap configuration options.
109     * @throws IllegalArgumentException
110     *         If the configuration is invalid.
111     */
112    public static Options configureOptions(final JsonValue config) {
113        final Options options = Options.defaultOptions();
114
115        options.set(READ_ON_UPDATE_POLICY,
116                    config.get("readOnUpdatePolicy").defaultTo(CONTROLS).as(enumConstant(ReadOnUpdatePolicy.class)));
117
118        // Default to false, even though it is supported by OpenDJ, because it requires additional permissions.
119        options.set(USE_SUBTREE_DELETE, config.get("useSubtreeDelete").defaultTo(false).asBoolean());
120
121        // Default to true because it is supported by OpenDJ and does not require additional permissions.
122        options.set(USE_PERMISSIVE_MODIFY, config.get("usePermissiveModify").defaultTo(false).asBoolean());
123
124        options.set(USE_MVCC, config.get("useMvcc").defaultTo(true).asBoolean());
125        options.set(MVCC_ATTRIBUTE, config.get("mvccAttribute").defaultTo("etag").asString());
126
127        return options;
128    }
129
130    /**
131     * Parses a list of Rest2Ldap resource definitions. The JSON configuration must have the following format:
132     * <p>
133     * <pre>
134     * "top": {
135     *     "isAbstract": true,
136     *     "properties": {
137     *         "_rev": {
138     *             "type": "simple"
139     *             "ldapAttribute": "etag",
140     *             "writability": "readOnly"
141     *         },
142     *         ...
143     *     },
144     *     ...
145     * },
146     * ...
147     * </pre>
148     * <p>
149     * See the sample configuration file for a detailed description of its content.
150     *
151     * @param config
152     *         The JSON configuration.
153     * @return The parsed list of Rest2Ldap resource definitions.
154     * @throws IllegalArgumentException
155     *         If the configuration is invalid.
156     */
157    public static List<Resource> configureResources(final JsonValue config) {
158        final JsonValue resourcesConfig = config.required().expect(Map.class);
159        final List<Resource> resources = new LinkedList<>();
160        for (final String resourceId : resourcesConfig.keys()) {
161            resources.add(configureResource(resourceId, resourcesConfig.get(resourceId)));
162        }
163        return resources;
164    }
165
166    /**
167     * Creates a new CREST {@link Router} using the provided endpoints configuration directory and Rest2Ldap options.
168     * The Rest2Ldap configuration typically has the following structure on disk:
169     * <ul>
170     * <li> config.json - contains the configuration for the LDAP connection factories and authorization
171     * <li> rest2ldap/rest2ldap.json - defines Rest2Ldap configuration options
172     * <li> rest2ldap/endpoints/{api} - a directory containing the endpoint's resource definitions for endpoint {api}
173     * <li> rest2ldap/endpoints/{api}/{resource-id}.json - the resource definitions for a specific version of API {api}.
174     * The name of the file, {resource-id}, determines which resource type definition in the mapping file will be
175     * used as the root resource.
176     * </ul>
177     *
178     * @param endpointsDirectory The directory representing the Rest2Ldap "endpoints" directory.
179     * @param options The Rest2Ldap configuration options.
180     * @return A new CREST {@link Router} configured using the provided options and endpoints.
181     * @throws IOException If the endpoints configuration cannot be read.
182     * @throws IllegalArgumentException
183     *         If the configuration is invalid.
184     */
185    public static Router configureEndpoints(final File endpointsDirectory, final Options options) throws IOException {
186        final Router pathRouter = new Router();
187
188        final File[] endpoints = endpointsDirectory.listFiles(new FileFilter() {
189            @Override
190            public boolean accept(final File pathname) {
191                return pathname.isDirectory() && pathname.canRead();
192            }
193        });
194
195        if (endpoints == null) {
196            throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINTS_DIRECTORY.get(endpointsDirectory));
197        }
198
199        for (final File endpoint : endpoints) {
200            final RequestHandler endpointHandler = configureEndpoint(endpoint, options);
201            pathRouter.addRoute(requestUriMatcher(STARTS_WITH, endpoint.getName()), endpointHandler);
202        }
203        return pathRouter;
204    }
205
206    /**
207     * Creates a new CREST {@link RequestHandler} representing a single endpoint whose configuration is defined in the
208     * provided {@code endpointDirectory} parameter. The directory should contain a separate file for each supported
209     * version of the REST endpoint. The name of the file, excluding the suffix, identifies the resource definition
210     * which acts as the entry point into the endpoint.
211     *
212     * @param endpointDirectory The directory containing the endpoint's resource definitions, e.g.
213     *                          rest2ldap/routes/api would contain definitions for the "api" endpoint.
214     * @param options The Rest2Ldap configuration options.
215     * @return A new CREST {@link RequestHandler} configured using the provided options and endpoint mappings.
216     * @throws IOException If the endpoint configuration cannot be read.
217     * @throws IllegalArgumentException If the configuration is invalid.
218     */
219    public static RequestHandler configureEndpoint(File endpointDirectory, Options options) throws IOException {
220        final Router versionRouter = new Router();
221        final File[] endpointVersions = endpointDirectory.listFiles(new FileFilter() {
222            @Override
223            public boolean accept(final File pathname) {
224                return pathname.isFile() && pathname.canRead() && pathname.getName().endsWith(".json");
225            }
226        });
227
228        if (endpointVersions == null) {
229            throw new LocalizedIllegalArgumentException(ERR_INVALID_ENDPOINT_DIRECTORY.get(endpointDirectory));
230        }
231
232        final List<String> supportedVersions = new ArrayList<>();
233        boolean hasWildCardVersion = false;
234        for (final File endpointVersion : endpointVersions) {
235            final JsonValue mappingConfig = readJson(endpointVersion);
236            final String version = mappingConfig.get("version").defaultTo("*").asString();
237            final List<Resource> resourceTypes = configureResources(mappingConfig.get("resourceTypes"));
238            final Rest2Ldap rest2Ldap = rest2Ldap(options, resourceTypes);
239
240            final String endpointVersionFileName = endpointVersion.getName();
241            final int endIndex = endpointVersionFileName.lastIndexOf('.');
242            final String rootResourceType = endpointVersionFileName.substring(0, endIndex);
243            final RequestHandler handler = rest2Ldap.newRequestHandlerFor(rootResourceType);
244
245            if (version.equals("*")) {
246                versionRouter.setDefaultRoute(handler);
247                hasWildCardVersion = true;
248            } else {
249                versionRouter.addRoute(version(version), handler);
250                supportedVersions.add(version);
251            }
252            logger.debug(INFO_REST2LDAP_CREATING_ENDPOINT.get(endpointDirectory.getName(), version));
253        }
254        if (!hasWildCardVersion) {
255            versionRouter.setDefaultRoute(new AbstractRequestHandler() {
256                @Override
257                protected <V> Promise<V, ResourceException> handleRequest(Context context, Request request) {
258                    final String message = ERR_BAD_API_RESOURCE_VERSION.get(request.getResourceVersion(),
259                                                                            joinAsString(", ", supportedVersions))
260                                                                       .toString();
261                    return new BadRequestException(message).asPromise();
262                }
263            });
264        }
265
266        // FIXME: Disable the warning header for now due to CREST-389 / CREST-390.
267        final ResourceApiVersionBehaviourManager behaviourManager = newResourceApiVersionBehaviourManager();
268        behaviourManager.setWarningEnabled(false);
269        return new FilterChain(versionRouter, resourceApiVersionContextFilter(behaviourManager));
270    }
271
272    static JsonValue readJson(final File resource) throws IOException {
273        try (InputStream in = new FileInputStream(resource)) {
274            return new JsonValue(readJsonLenient(in));
275        }
276    }
277
278    private static Resource configureResource(final String resourceId, final JsonValue config) {
279        final Resource resource = resource(resourceId)
280                .isAbstract(config.get("isAbstract").defaultTo(false).asBoolean())
281                .superType(config.get("superType").asString())
282                .objectClasses(config.get("objectClasses")
283                                     .defaultTo(emptyList()).asList(String.class).toArray(new String[0]))
284                .supportedActions(config.get("supportedActions")
285                                        .defaultTo(emptyList())
286                                        .as(setOf(enumConstant(Action.class))).toArray(new Action[0]))
287                .resourceTypeProperty(config.get("resourceTypeProperty").as(pointer()))
288                .includeAllUserAttributesByDefault(config.get("includeAllUserAttributesByDefault")
289                                                         .defaultTo(false).asBoolean())
290                .excludedDefaultUserAttributes(config.get("excludedDefaultUserAttributes")
291                                                     .defaultTo(Collections.emptyList()).asList(String.class));
292
293        final JsonValue properties = config.get("properties").expect(Map.class);
294        for (final String property : properties.keys()) {
295            resource.property(property, configurePropertyMapper(properties.get(property), property));
296        }
297
298        final JsonValue subResources = config.get("subResources").expect(Map.class);
299        for (final String urlTemplate : subResources.keys()) {
300            resource.subResource(configureSubResource(urlTemplate, subResources.get(urlTemplate)));
301        }
302
303        return resource;
304    }
305
306    private enum NamingStrategyType { CLIENTDNNAMING, CLIENTNAMING, SERVERNAMING }
307    private enum SubResourceType { COLLECTION, SINGLETON }
308
309    private static SubResource configureSubResource(final String urlTemplate, final JsonValue config) {
310        final String dnTemplate = config.get("dnTemplate").defaultTo("").asString();
311        final Boolean isReadOnly = config.get("isReadOnly").defaultTo(false).asBoolean();
312        final String resourceId = config.get("resource").required().asString();
313
314        if (config.get("type").required().as(enumConstant(SubResourceType.class)) == SubResourceType.COLLECTION) {
315            final String[] glueObjectClasses = config.get("glueObjectClasses")
316                                                     .defaultTo(emptyList())
317                                                     .asList(String.class)
318                                                     .toArray(new String[0]);
319
320            final SubResourceCollection collection = collectionOf(resourceId).urlTemplate(urlTemplate)
321                                                                             .dnTemplate(dnTemplate)
322                                                                             .isReadOnly(isReadOnly)
323                                                                             .glueObjectClasses(glueObjectClasses);
324
325            final JsonValue namingStrategy = config.get("namingStrategy").required();
326            switch (namingStrategy.get("type").required().as(enumConstant(NamingStrategyType.class))) {
327            case CLIENTDNNAMING:
328                collection.useClientDnNaming(namingStrategy.get("dnAttribute").required().asString());
329                break;
330            case CLIENTNAMING:
331                collection.useClientNaming(namingStrategy.get("dnAttribute").required().asString(),
332                                           namingStrategy.get("idAttribute").required().asString());
333                break;
334            case SERVERNAMING:
335                collection.useServerNaming(namingStrategy.get("dnAttribute").required().asString(),
336                                           namingStrategy.get("idAttribute").required().asString());
337                break;
338            }
339
340            return collection;
341        } else {
342            return singletonOf(resourceId).urlTemplate(urlTemplate).dnTemplate(dnTemplate).isReadOnly(isReadOnly);
343        }
344    }
345
346    private static PropertyMapper configurePropertyMapper(final JsonValue mapper, final String defaultLdapAttribute) {
347        switch (mapper.get("type").required().asString()) {
348        case "resourceType":
349            return resourceType();
350        case "constant":
351            return constant(mapper.get("value").getObject());
352        case "simple":
353            return simple(mapper.get("ldapAttribute").defaultTo(defaultLdapAttribute).required().asString())
354                    .defaultJsonValue(mapper.get("defaultJsonValue").getObject())
355                    .isBinary(mapper.get("isBinary").defaultTo(false).asBoolean())
356                    .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean())
357                    .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean())
358                    .writability(parseWritability(mapper));
359        case "reference":
360            final String ldapAttribute = mapper.get("ldapAttribute")
361                                               .defaultTo(defaultLdapAttribute).required().asString();
362            final String baseDN = mapper.get("baseDn").required().asString();
363            final String primaryKey = mapper.get("primaryKey").required().asString();
364            final PropertyMapper m = configurePropertyMapper(mapper.get("mapper").required(), primaryKey);
365            return reference(ldapAttribute, baseDN, primaryKey, m)
366                    .isRequired(mapper.get("isRequired").defaultTo(false).asBoolean())
367                    .isMultiValued(mapper.get("isMultiValued").defaultTo(false).asBoolean())
368                    .searchFilter(mapper.get("searchFilter").defaultTo("(objectClass=*)").asString())
369                    .writability(parseWritability(mapper));
370        case "object":
371            final JsonValue properties = mapper.get("properties");
372            final ObjectPropertyMapper object = object();
373            for (final String attribute : properties.keys()) {
374                object.property(attribute, configurePropertyMapper(properties.get(attribute), attribute));
375            }
376            return object;
377        default:
378            throw newJsonValueException(mapper, ERR_CONFIG_NO_MAPPING_IN_CONFIGURATION.get(
379                    "constant, simple, reference, object"));
380        }
381    }
382
383    private static WritabilityPolicy parseWritability(final JsonValue mapper) {
384        return mapper.get("writability").defaultTo("readWrite").as(enumConstant(WritabilityPolicy.class));
385    }
386
387    /** Indicates whether LDAP client connections should use SSL or StartTLS. */
388    private enum ConnectionSecurity { NONE, SSL, STARTTLS }
389
390    /** Specifies the mechanism which will be used for trusting certificates presented by the LDAP server. */
391    private enum TrustManagerType { TRUSTALL, JVM, FILE }
392
393    /** Specifies the type of key-store to use when performing SSL client authentication. */
394    private enum KeyManagerType { JVM, FILE, PKCS11 }
395
396    /**
397     * Configures a {@link X509KeyManager} using the provided JSON configuration.
398     *
399     * @param configuration
400     *         The JSON object containing the key manager configuration.
401     * @return The configured key manager.
402     */
403    public static X509KeyManager configureKeyManager(final JsonValue configuration) {
404        try {
405            return configureKeyManager(configuration, KeyManagerType.JVM);
406        } catch (GeneralSecurityException | IOException e) {
407            throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER.get(
408                    configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
409        }
410    }
411
412    private static X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing)
413            throws GeneralSecurityException, IOException {
414        final KeyManagerType keyManagerType = config.get("keyManager")
415                                                    .defaultTo(defaultIfMissing)
416                                                    .as(enumConstant(KeyManagerType.class));
417        switch (keyManagerType) {
418        case JVM:
419            return useJvmDefaultKeyStore();
420        case FILE:
421            final String fileName = config.get("fileBasedKeyManagerFile").required().asString();
422            final String passwordFile = config.get("fileBasedKeyManagerPasswordFile").asString();
423            final String password = passwordFile != null
424                    ? readPasswordFromFile(passwordFile) : config.get("fileBasedKeyManagerPassword").asString();
425            final String type = config.get("fileBasedKeyManagerType").asString();
426            final String provider = config.get("fileBasedKeyManagerProvider").asString();
427            return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, type, provider);
428        case PKCS11:
429            final String pkcs11PasswordFile = config.get("pkcs11KeyManagerPasswordFile").asString();
430            return usePKCS11Token(pkcs11PasswordFile != null
431                                          ? readPasswordFromFile(pkcs11PasswordFile).toCharArray() : null);
432        default:
433            throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType);
434        }
435    }
436
437    private static String readPasswordFromFile(String fileName) throws IOException {
438        try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) {
439            return reader.readLine();
440        }
441    }
442
443    /**
444     * Configures a {@link TrustManager} using the provided JSON configuration.
445     *
446     * @param configuration
447     *         The JSON object containing the trust manager configuration.
448     * @return The configured trust manager.
449     */
450    public static TrustManager configureTrustManager(final JsonValue configuration) {
451        try {
452            return configureTrustManager(configuration, TrustManagerType.JVM);
453        } catch (GeneralSecurityException | IOException e) {
454            throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER.get(
455                    configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
456        }
457    }
458
459    private static TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing)
460            throws GeneralSecurityException, IOException {
461        final TrustManagerType trustManagerType = config.get("trustManager")
462                                                        .defaultTo(defaultIfMissing)
463                                                        .as(enumConstant(TrustManagerType.class));
464        switch (trustManagerType) {
465        case TRUSTALL:
466            return trustAll();
467        case JVM:
468            return null;
469        case FILE:
470            final String fileName = config.get("fileBasedTrustManagerFile").required().asString();
471            final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString();
472            final String password = passwordFile != null
473                    ? readPasswordFromFile(passwordFile) : config.get("fileBasedTrustManagerPassword").asString();
474            final String type = config.get("fileBasedTrustManagerType").asString();
475            return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type);
476        default:
477            throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType);
478        }
479    }
480
481    /**
482     * Creates a new connection factory using the named configuration in the provided JSON list of factory
483     * configurations. See the sample configuration file for a detailed description of its content.
484     *
485     * @param configuration
486     *         The JSON configuration.
487     * @param name
488     *         The name of the connection factory configuration to be parsed.
489     * @param trustManager
490     *            The trust manager to use for secure connection. Can be {@code null}
491     *            to use the default JVM trust manager.
492     * @param keyManager
493     *            The key manager to use for secure connection. Can be {@code null}
494     *            to use the default JVM key manager.
495     * @param providerClassLoader
496     *         The {@link ClassLoader} used to fetch the {@link org.forgerock.opendj.ldap.spi.TransportProvider}. This
497     *         can be useful in OSGI environments.
498     * @return A new connection factory using the provided JSON configuration.
499     * @throws IllegalArgumentException
500     *         If the configuration is invalid.
501     */
502    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
503                                                               final String name,
504                                                               final TrustManager trustManager,
505                                                               final X509KeyManager keyManager,
506                                                               final ClassLoader providerClassLoader) {
507        final JsonValue normalizedConfiguration = normalizeConnectionFactory(configuration, name, 0);
508        return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader);
509    }
510
511    /**
512     * Creates a new connection factory using the named configuration in the provided JSON list of factory
513     * configurations. See the sample configuration file for a detailed description of its content.
514     *
515     * @param configuration
516     *         The JSON configuration.
517     * @param name
518     *         The name of the connection factory configuration to be parsed.
519     * @param trustManager
520     *            The trust manager to use for secure connection. Can be {@code null}
521     *            to use the default JVM trust manager.
522     * @param keyManager
523     *            The key manager to use for secure connection. Can be {@code null}
524     *            to use the default JVM key manager.
525     * @return A new connection factory using the provided JSON configuration.
526     * @throws IllegalArgumentException
527     *         If the configuration is invalid.
528     */
529    public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
530                                                               final String name,
531                                                               final TrustManager trustManager,
532                                                               final X509KeyManager keyManager) {
533        return configureConnectionFactory(configuration, name, trustManager, keyManager, null);
534    }
535
536    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
537                                                                final TrustManager trustManager,
538                                                                final X509KeyManager keyManager,
539                                                                final ClassLoader providerClassLoader) {
540        final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
541        final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
542
543        final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong();
544        final Duration heartBeatTimeout = duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS);
545
546        final Options options = Options.defaultOptions()
547                                       .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader)
548                                       .set(HEARTBEAT_ENABLED, true)
549                                       .set(HEARTBEAT_INTERVAL, heartBeatInterval)
550                                       .set(HEARTBEAT_TIMEOUT, heartBeatTimeout)
551                                       .set(LOAD_BALANCER_MONITORING_INTERVAL, heartBeatInterval);
552
553        // Parse pool parameters,
554        final int connectionPoolSize =
555                Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1);
556
557        // Parse authentication parameters.
558        if (configuration.isDefined("authentication")) {
559            final JsonValue authn = configuration.get("authentication");
560            if (authn.isDefined("simple")) {
561                final JsonValue simple = authn.get("simple");
562                final BindRequest bindRequest =
563                        Requests.newSimpleBindRequest(simple.get("bindDn").required().asString(),
564                                                      simple.get("bindPassword").required().asString().toCharArray());
565                options.set(AUTHN_BIND_REQUEST, bindRequest);
566            } else {
567                throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_AUTHENTICATION.get());
568            }
569        }
570
571        // Parse SSL/StartTLS parameters.
572        final ConnectionSecurity connectionSecurity = configuration.get("connectionSecurity")
573                                                                   .defaultTo(ConnectionSecurity.NONE)
574                                                                   .as(enumConstant(ConnectionSecurity.class));
575        if (connectionSecurity != ConnectionSecurity.NONE) {
576            try {
577                // Configure SSL.
578                final SSLContextBuilder builder = new SSLContextBuilder();
579                builder.setTrustManager(trustManager);
580                final String sslCertAlias = configuration.get("sslCertAlias").asString();
581                builder.setKeyManager(sslCertAlias != null
582                                              ? useSingleCertificate(sslCertAlias, keyManager)
583                                              : keyManager);
584                options.set(SSL_CONTEXT, builder.getSSLContext());
585                options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS);
586            } catch (GeneralSecurityException e) {
587                // Rethrow as unchecked exception.
588                throw new IllegalArgumentException(e);
589            }
590        }
591
592        // Parse primary data center.
593        final JsonValue primaryLdapServers = configuration.get("primaryLdapServers");
594        if (!primaryLdapServers.isList() || primaryLdapServers.size() == 0) {
595            throw new IllegalArgumentException("No primaryLdapServers");
596        }
597        final ConnectionFactory primary = parseLdapServers(primaryLdapServers, connectionPoolSize, options);
598
599        // Parse secondary data center(s).
600        final JsonValue secondaryLdapServers = configuration.get("secondaryLdapServers");
601        ConnectionFactory secondary = null;
602        if (secondaryLdapServers.isList()) {
603            if (secondaryLdapServers.size() > 0) {
604                secondary = parseLdapServers(secondaryLdapServers, connectionPoolSize, options);
605            }
606        } else if (!secondaryLdapServers.isNull()) {
607            throw new LocalizedIllegalArgumentException(ERR_CONFIG_INVALID_SECONDARY_LDAP_SERVER.get());
608        }
609
610        // Create fail-over.
611        if (secondary != null) {
612            return newFailoverLoadBalancer(asList(primary, secondary), options);
613        } else {
614            return primary;
615        }
616    }
617
618    private static JsonValue normalizeConnectionFactory(final JsonValue configuration,
619                                                        final String name, final int depth) {
620        // Protect against infinite recursion in the configuration.
621        if (depth > 100) {
622            throw new LocalizedIllegalArgumentException(ERR_CONFIG_SERVER_CIRCULAR_DEPENDENCIES.get(name));
623        }
624
625        final JsonValue current = configuration.get(name).required();
626        if (current.isDefined("inheritFrom")) {
627            // Inherit missing fields from inherited configuration.
628            final JsonValue parent =
629                    normalizeConnectionFactory(configuration,
630                                               current.get("inheritFrom").asString(), depth + 1);
631            final Map<String, Object> normalized = new LinkedHashMap<>(parent.asMap());
632            normalized.putAll(current.asMap());
633            normalized.remove("inheritFrom");
634            return new JsonValue(normalized);
635        } else {
636            // No normalization required.
637            return current;
638        }
639    }
640
641    private static ConnectionFactory parseLdapServers(JsonValue config, int poolSize, Options options) {
642        final List<ConnectionFactory> servers = new ArrayList<>(config.size());
643        for (final JsonValue server : config) {
644            final String host = server.get("hostname").required().asString();
645            final int port = server.get("port").required().asInteger();
646            final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
647            if (poolSize > 1) {
648                servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS));
649            } else {
650                servers.add(factory);
651            }
652        }
653        if (servers.size() > 1) {
654            return newRoundRobinLoadBalancer(servers, options);
655        } else {
656            return servers.get(0);
657        }
658    }
659
660    private Rest2LdapJsonConfigurator() {
661        // Prevent instantiation.
662    }
663}