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