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}