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}