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}