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-2016 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            connection.configureBlocking(true);
109
110            final GrizzlyLDAPConnection ldapConnection =
111                    new GrizzlyLDAPConnection(connection, GrizzlyLDAPConnectionFactory.this);
112            timeoutChecker.get().addListener(ldapConnection);
113            clientFilter.registerConnection(connection, ldapConnection);
114            return ldapConnection;
115        }
116
117        private LdapException adaptConnectionException(Throwable t) {
118            if (!(t instanceof LdapException) && t instanceof ExecutionException) {
119                t = t.getCause() != null ? t.getCause() : t;
120            }
121            if (t instanceof LdapException) {
122                return (LdapException) t;
123            } else {
124                return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, t.getMessage(), t);
125            }
126        }
127
128        @Override
129        public long handleTimeout(final long currentTime) {
130            if (timeoutEndTime == 0) {
131                return 0;
132            } else if (timeoutEndTime > currentTime) {
133                return timeoutEndTime - currentTime;
134            } else {
135                promise.handleException(newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
136                        LDAP_CONNECTION_CONNECT_TIMEOUT.get(getSocketAddress(), getTimeout()).toString()));
137                return 0;
138            }
139        }
140
141        @Override
142        public long getTimeout() {
143            final Duration duration = options.get(CONNECT_TIMEOUT);
144            return duration.isUnlimited() ? 0L : duration.to(TimeUnit.MILLISECONDS);
145        }
146    }
147
148    private final LDAPClientFilter clientFilter;
149    private final FilterChain defaultFilterChain;
150    private final Options options;
151    private final String host;
152    private final int port;
153
154    /**
155     * Prevents the transport and timeoutChecker being released when there are
156     * remaining references (this factory or any connections). It is initially
157     * set to 1 because this factory has a reference.
158     */
159    private final AtomicInteger referenceCount = new AtomicInteger(1);
160
161    /**
162     * Indicates whether this factory has been closed or not.
163     */
164    private final AtomicBoolean isClosed = new AtomicBoolean();
165
166    private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
167    private final ReferenceCountedObject<TimeoutChecker>.Reference timeoutChecker = TIMEOUT_CHECKER.acquire();
168
169    /**
170     * Grizzly TCP Transport NIO implementation to use for connections. If {@code null}, default transport will be
171     * used.
172     */
173    public static final Option<TCPNIOTransport> GRIZZLY_TRANSPORT = Option.of(TCPNIOTransport.class, null);
174
175    /**
176     * Creates a new LDAP connection factory based on Grizzly which can be used to create connections to the Directory
177     * Server at the provided host and port address using provided connection options.
178     *
179     * @param host
180     *         The hostname of the Directory Server to connect to.
181     * @param port
182     *         The port number of the Directory Server to connect to.
183     * @param options
184     *         The LDAP connection options to use when creating connections.
185     */
186    public GrizzlyLDAPConnectionFactory(final String host, final int port, final Options options) {
187        this.transport = DEFAULT_TRANSPORT.acquireIfNull(options.get(GRIZZLY_TRANSPORT));
188        this.host = host;
189        this.port = port;
190        this.options = options;
191        this.clientFilter = new LDAPClientFilter(options.get(LDAP_DECODE_OPTIONS), 0);
192        this.defaultFilterChain = buildFilterChain(this.transport.get().getProcessor(), clientFilter);
193    }
194
195    @Override
196    public void close() {
197        if (isClosed.compareAndSet(false, true)) {
198            releaseTransportAndTimeoutChecker();
199        }
200    }
201
202    @Override
203    public Promise<LDAPConnectionImpl, LdapException> getConnectionAsync() {
204        acquireTransportAndTimeoutChecker(); // Protect resources.
205        final SocketConnectorHandler connectorHandler = TCPNIOConnectorHandler.builder(transport.get())
206                                                                              .processor(defaultFilterChain)
207                                                                              .build();
208        final PromiseImpl<LDAPConnectionImpl, LdapException> promise = PromiseImpl.create();
209        connectorHandler.connect(getSocketAddress(), new CompletionHandlerAdapter(promise));
210        return promise;
211    }
212
213    @Override
214    public InetSocketAddress getSocketAddress() {
215        return new InetSocketAddress(host, port);
216    }
217
218    @Override
219    public String getHostName() {
220        return host;
221    }
222
223    @Override
224    public int getPort() {
225        return port;
226    }
227
228    TimeoutChecker getTimeoutChecker() {
229        return timeoutChecker.get();
230    }
231
232    Options getLDAPOptions() {
233        return options;
234    }
235
236    void releaseTransportAndTimeoutChecker() {
237        if (referenceCount.decrementAndGet() == 0) {
238            transport.release();
239            timeoutChecker.release();
240        }
241    }
242
243    private void acquireTransportAndTimeoutChecker() {
244        /*
245         * If the factory is not closed then we need to prevent the resources
246         * (transport, timeout checker) from being released while the connection
247         * attempt is in progress.
248         */
249        referenceCount.incrementAndGet();
250        if (isClosed.get()) {
251            releaseTransportAndTimeoutChecker();
252            throw new IllegalStateException("Attempted to get a connection after factory close");
253        }
254    }
255}