/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. */ package org.forgerock.opendj.grizzly; import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_CONNECT_TIMEOUT; import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.DEFAULT_TRANSPORT; import static org.forgerock.opendj.grizzly.GrizzlyUtils.buildFilterChain; import static org.forgerock.opendj.grizzly.GrizzlyUtils.configureConnection; import static org.forgerock.opendj.ldap.LDAPConnectionFactory.CONNECT_TIMEOUT; import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS; import static org.forgerock.opendj.ldap.LdapException.newLdapException; import static org.forgerock.opendj.ldap.TimeoutChecker.TIMEOUT_CHECKER; import java.net.InetSocketAddress; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.LdapException; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.TimeoutChecker; import org.forgerock.opendj.ldap.TimeoutEventListener; import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl; import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl; import org.forgerock.util.Option; import org.forgerock.util.Options; import org.forgerock.util.promise.Promise; import org.forgerock.util.promise.PromiseImpl; import org.forgerock.util.time.Duration; import org.glassfish.grizzly.CompletionHandler; import org.glassfish.grizzly.Connection; import org.glassfish.grizzly.SocketConnectorHandler; import org.glassfish.grizzly.filterchain.FilterChain; import org.glassfish.grizzly.nio.transport.TCPNIOConnectorHandler; import org.glassfish.grizzly.nio.transport.TCPNIOTransport; import com.forgerock.opendj.util.ReferenceCountedObject; /** * LDAP connection factory implementation using Grizzly for transport. */ public final class GrizzlyLDAPConnectionFactory implements LDAPConnectionFactoryImpl { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * Adapts a Grizzly connection completion handler to an LDAP connection promise. */ @SuppressWarnings("rawtypes") private final class CompletionHandlerAdapter implements CompletionHandler, TimeoutEventListener { private final PromiseImpl promise; private final long timeoutEndTime; private CompletionHandlerAdapter(final PromiseImpl promise) { this.promise = promise; final long timeoutMS = getTimeout(); this.timeoutEndTime = timeoutMS > 0 ? System.currentTimeMillis() + timeoutMS : 0; timeoutChecker.get().addListener(this); } @Override public void cancelled() { // Ignore this. } @Override public void completed(final Connection result) { // Adapt the connection. final GrizzlyLDAPConnection connection = adaptConnection(result); timeoutChecker.get().removeListener(this); if (!promise.tryHandleResult(connection)) { // The connection has been either cancelled or it has timed out. connection.close(); } } @Override public void failed(final Throwable throwable) { // Adapt and forward. timeoutChecker.get().removeListener(this); promise.handleException(adaptConnectionException(throwable)); releaseTransportAndTimeoutChecker(); } @Override public void updated(final Connection result) { // Ignore this. } private GrizzlyLDAPConnection adaptConnection(final Connection connection) { configureConnection(connection, logger, options); connection.configureBlocking(true); final GrizzlyLDAPConnection ldapConnection = new GrizzlyLDAPConnection(connection, GrizzlyLDAPConnectionFactory.this); timeoutChecker.get().addListener(ldapConnection); clientFilter.registerConnection(connection, ldapConnection); return ldapConnection; } private LdapException adaptConnectionException(Throwable t) { if (!(t instanceof LdapException) && t instanceof ExecutionException) { t = t.getCause() != null ? t.getCause() : t; } if (t instanceof LdapException) { return (LdapException) t; } else { return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, t.getMessage(), t); } } @Override public long handleTimeout(final long currentTime) { if (timeoutEndTime == 0) { return 0; } else if (timeoutEndTime > currentTime) { return timeoutEndTime - currentTime; } else { promise.handleException(newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, LDAP_CONNECTION_CONNECT_TIMEOUT.get(getSocketAddress(), getTimeout()).toString())); return 0; } } @Override public long getTimeout() { final Duration duration = options.get(CONNECT_TIMEOUT); return duration.isUnlimited() ? 0L : duration.to(TimeUnit.MILLISECONDS); } } private final LDAPClientFilter clientFilter; private final FilterChain defaultFilterChain; private final Options options; private final String host; private final int port; /** * Prevents the transport and timeoutChecker being released when there are * remaining references (this factory or any connections). It is initially * set to 1 because this factory has a reference. */ private final AtomicInteger referenceCount = new AtomicInteger(1); /** * Indicates whether this factory has been closed or not. */ private final AtomicBoolean isClosed = new AtomicBoolean(); private final ReferenceCountedObject.Reference transport; private final ReferenceCountedObject.Reference timeoutChecker = TIMEOUT_CHECKER.acquire(); /** * Grizzly TCP Transport NIO implementation to use for connections. If {@code null}, default transport will be * used. */ public static final Option GRIZZLY_TRANSPORT = Option.of(TCPNIOTransport.class, null); /** * Creates a new LDAP connection factory based on Grizzly which can be used to create connections to the Directory * Server at the provided host and port address using provided connection options. * * @param host * The hostname of the Directory Server to connect to. * @param port * The port number of the Directory Server to connect to. * @param options * The LDAP connection options to use when creating connections. */ public GrizzlyLDAPConnectionFactory(final String host, final int port, final Options options) { this.transport = DEFAULT_TRANSPORT.acquireIfNull(options.get(GRIZZLY_TRANSPORT)); this.host = host; this.port = port; this.options = options; this.clientFilter = new LDAPClientFilter(options.get(LDAP_DECODE_OPTIONS), 0); this.defaultFilterChain = buildFilterChain(this.transport.get().getProcessor(), clientFilter); } @Override public void close() { if (isClosed.compareAndSet(false, true)) { releaseTransportAndTimeoutChecker(); } } @Override public Promise getConnectionAsync() { acquireTransportAndTimeoutChecker(); // Protect resources. final SocketConnectorHandler connectorHandler = TCPNIOConnectorHandler.builder(transport.get()) .processor(defaultFilterChain) .build(); final PromiseImpl promise = PromiseImpl.create(); connectorHandler.connect(getSocketAddress(), new CompletionHandlerAdapter(promise)); return promise; } @Override public InetSocketAddress getSocketAddress() { return new InetSocketAddress(host, port); } @Override public String getHostName() { return host; } @Override public int getPort() { return port; } TimeoutChecker getTimeoutChecker() { return timeoutChecker.get(); } Options getLDAPOptions() { return options; } void releaseTransportAndTimeoutChecker() { if (referenceCount.decrementAndGet() == 0) { transport.release(); timeoutChecker.release(); } } private void acquireTransportAndTimeoutChecker() { /* * If the factory is not closed then we need to prevent the resources * (transport, timeout checker) from being released while the connection * attempt is in progress. */ referenceCount.incrementAndGet(); if (isClosed.get()) { releaseTransportAndTimeoutChecker(); throw new IllegalStateException("Attempted to get a connection after factory close"); } } }