001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027
028package org.forgerock.opendj.grizzly;
029
030import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_CONNECT_TIMEOUT;
031import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.DEFAULT_TRANSPORT;
032import static org.forgerock.opendj.grizzly.GrizzlyUtils.buildFilterChain;
033import static org.forgerock.opendj.grizzly.GrizzlyUtils.configureConnection;
034import static org.forgerock.opendj.ldap.LDAPConnectionFactory.CONNECT_TIMEOUT;
035import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
036import static org.forgerock.opendj.ldap.LdapException.newLdapException;
037import static org.forgerock.opendj.ldap.TimeoutChecker.TIMEOUT_CHECKER;
038
039import java.net.InetSocketAddress;
040import java.util.concurrent.ExecutionException;
041import java.util.concurrent.TimeUnit;
042import java.util.concurrent.atomic.AtomicBoolean;
043import java.util.concurrent.atomic.AtomicInteger;
044
045import org.forgerock.i18n.slf4j.LocalizedLogger;
046import org.forgerock.opendj.ldap.LdapException;
047import org.forgerock.opendj.ldap.ResultCode;
048import org.forgerock.opendj.ldap.TimeoutChecker;
049import org.forgerock.opendj.ldap.TimeoutEventListener;
050import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
051import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
052import org.forgerock.util.Option;
053import org.forgerock.util.Options;
054import org.forgerock.util.promise.Promise;
055import org.forgerock.util.promise.PromiseImpl;
056import org.forgerock.util.time.Duration;
057import org.glassfish.grizzly.CompletionHandler;
058import org.glassfish.grizzly.Connection;
059import org.glassfish.grizzly.SocketConnectorHandler;
060import org.glassfish.grizzly.filterchain.FilterChain;
061import org.glassfish.grizzly.nio.transport.TCPNIOConnectorHandler;
062import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
063
064import com.forgerock.opendj.util.ReferenceCountedObject;
065
066/**
067 * LDAP connection factory implementation using Grizzly for transport.
068 */
069public final class GrizzlyLDAPConnectionFactory implements LDAPConnectionFactoryImpl {
070    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
071
072    /**
073     * Adapts a Grizzly connection completion handler to an LDAP connection promise.
074     */
075    @SuppressWarnings("rawtypes")
076    private final class CompletionHandlerAdapter implements CompletionHandler<Connection>, TimeoutEventListener {
077        private final PromiseImpl<LDAPConnectionImpl, LdapException> promise;
078        private final long timeoutEndTime;
079
080        private CompletionHandlerAdapter(final PromiseImpl<LDAPConnectionImpl, LdapException> promise) {
081            this.promise = promise;
082            final long timeoutMS = getTimeout();
083            this.timeoutEndTime = timeoutMS > 0 ? System.currentTimeMillis() + timeoutMS : 0;
084            timeoutChecker.get().addListener(this);
085        }
086
087        @Override
088        public void cancelled() {
089            // Ignore this.
090        }
091
092        @Override
093        public void completed(final Connection result) {
094            // Adapt the connection.
095            final GrizzlyLDAPConnection connection = adaptConnection(result);
096            timeoutChecker.get().removeListener(this);
097            if (!promise.tryHandleResult(connection)) {
098                // The connection has been either cancelled or it has timed out.
099                connection.close();
100            }
101        }
102
103        @Override
104        public void failed(final Throwable throwable) {
105            // Adapt and forward.
106            timeoutChecker.get().removeListener(this);
107            promise.handleException(adaptConnectionException(throwable));
108            releaseTransportAndTimeoutChecker();
109        }
110
111        @Override
112        public void updated(final Connection result) {
113            // Ignore this.
114        }
115
116        private GrizzlyLDAPConnection adaptConnection(final Connection<?> connection) {
117            configureConnection(connection, logger, options);
118
119            final GrizzlyLDAPConnection ldapConnection =
120                    new GrizzlyLDAPConnection(connection, GrizzlyLDAPConnectionFactory.this);
121            timeoutChecker.get().addListener(ldapConnection);
122            clientFilter.registerConnection(connection, ldapConnection);
123            return ldapConnection;
124        }
125
126        private LdapException adaptConnectionException(Throwable t) {
127            if (!(t instanceof LdapException) && t instanceof ExecutionException) {
128                t = t.getCause() != null ? t.getCause() : t;
129            }
130            if (t instanceof LdapException) {
131                return (LdapException) t;
132            } else {
133                return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, t.getMessage(), t);
134            }
135        }
136
137        @Override
138        public long handleTimeout(final long currentTime) {
139            if (timeoutEndTime == 0) {
140                return 0;
141            } else if (timeoutEndTime > currentTime) {
142                return timeoutEndTime - currentTime;
143            } else {
144                promise.handleException(newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
145                        LDAP_CONNECTION_CONNECT_TIMEOUT.get(getSocketAddress(), getTimeout()).toString()));
146                return 0;
147            }
148        }
149
150        @Override
151        public long getTimeout() {
152            final Duration duration = options.get(CONNECT_TIMEOUT);
153            return duration.isUnlimited() ? 0L : duration.to(TimeUnit.MILLISECONDS);
154        }
155    }
156
157    private final LDAPClientFilter clientFilter;
158    private final FilterChain defaultFilterChain;
159    private final Options options;
160    private final String host;
161    private final int port;
162
163    /**
164     * Prevents the transport and timeoutChecker being released when there are
165     * remaining references (this factory or any connections). It is initially
166     * set to 1 because this factory has a reference.
167     */
168    private final AtomicInteger referenceCount = new AtomicInteger(1);
169
170    /**
171     * Indicates whether this factory has been closed or not.
172     */
173    private final AtomicBoolean isClosed = new AtomicBoolean();
174
175    private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
176    private final ReferenceCountedObject<TimeoutChecker>.Reference timeoutChecker = TIMEOUT_CHECKER.acquire();
177
178    /**
179     * Grizzly TCP Transport NIO implementation to use for connections. If {@code null}, default transport will be
180     * used.
181     */
182    public static final Option<TCPNIOTransport> GRIZZLY_TRANSPORT = Option.of(TCPNIOTransport.class, null);
183
184    /**
185     * Creates a new LDAP connection factory based on Grizzly which can be used to create connections to the Directory
186     * Server at the provided host and port address using provided connection options.
187     *
188     * @param host
189     *         The hostname of the Directory Server to connect to.
190     * @param port
191     *         The port number of the Directory Server to connect to.
192     * @param options
193     *         The LDAP connection options to use when creating connections.
194     */
195    public GrizzlyLDAPConnectionFactory(final String host, final int port, final Options options) {
196        this.transport = DEFAULT_TRANSPORT.acquireIfNull(options.get(GRIZZLY_TRANSPORT));
197        this.host = host;
198        this.port = port;
199        this.options = options;
200        this.clientFilter = new LDAPClientFilter(options.get(LDAP_DECODE_OPTIONS), 0);
201        this.defaultFilterChain = buildFilterChain(this.transport.get().getProcessor(), clientFilter);
202    }
203
204    @Override
205    public void close() {
206        if (isClosed.compareAndSet(false, true)) {
207            releaseTransportAndTimeoutChecker();
208        }
209    }
210
211    @Override
212    public Promise<LDAPConnectionImpl, LdapException> getConnectionAsync() {
213        acquireTransportAndTimeoutChecker(); // Protect resources.
214        final SocketConnectorHandler connectorHandler = TCPNIOConnectorHandler.builder(transport.get())
215                                                                              .processor(defaultFilterChain)
216                                                                              .build();
217        final PromiseImpl<LDAPConnectionImpl, LdapException> promise = PromiseImpl.create();
218        connectorHandler.connect(getSocketAddress(), new CompletionHandlerAdapter(promise));
219        return promise;
220    }
221
222    @Override
223    public InetSocketAddress getSocketAddress() {
224        return new InetSocketAddress(host, port);
225    }
226
227    @Override
228    public String getHostName() {
229        return host;
230    }
231
232    @Override
233    public int getPort() {
234        return port;
235    }
236
237    TimeoutChecker getTimeoutChecker() {
238        return timeoutChecker.get();
239    }
240
241    Options getLDAPOptions() {
242        return options;
243    }
244
245    void releaseTransportAndTimeoutChecker() {
246        if (referenceCount.decrementAndGet() == 0) {
247            transport.release();
248            timeoutChecker.release();
249        }
250    }
251
252    private void acquireTransportAndTimeoutChecker() {
253        /*
254         * If the factory is not closed then we need to prevent the resources
255         * (transport, timeout checker) from being released while the connection
256         * attempt is in progress.
257         */
258        referenceCount.incrementAndGet();
259        if (isClosed.get()) {
260            releaseTransportAndTimeoutChecker();
261            throw new IllegalStateException("Attempted to get a connection after factory close");
262        }
263    }
264}