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 2013-2015 ForgeRock AS. 025 */ 026package org.forgerock.opendj.ldap.spi; 027 028import java.util.LinkedList; 029import java.util.List; 030 031import org.forgerock.opendj.ldap.ConnectionEventListener; 032import org.forgerock.opendj.ldap.LdapException; 033import org.forgerock.opendj.ldap.responses.ExtendedResult; 034 035/** 036 * This class can be used to manage the internal state of a connection, ensuring 037 * valid and atomic state transitions, as well as connection event listener 038 * notification. There are 4 states: 039 * <ul> 040 * <li>connection is <b>valid</b> (isClosed()=false, isFailed()=false): can fail 041 * or be closed 042 * <li>connection has failed due to an <b>error</b> (isClosed()=false, 043 * isFailed()=true): can be closed 044 * <li>connection has been <b>closed</b> by the application (isClosed()=true, 045 * isFailed()=false): terminal state 046 * <li>connection has failed due to an <b>error</b> and has been <b>closed</b> 047 * by the application (isClosed()=true, isFailed()=true): terminal state 048 * </ul> 049 * All methods are synchronized and container classes may also synchronize on 050 * the state where needed. The state transition methods, 051 * {@link #notifyConnectionClosed()} and 052 * {@link #notifyConnectionError(boolean, LdapException)}, correspond to 053 * methods in the {@link ConnectionEventListener} interface except that they 054 * return a boolean indicating whether the transition was successful or not. 055 */ 056public final class ConnectionState { 057 /* 058 * FIXME: The synchronization in this class has been kept simple for now. 059 * However, ideally we should notify listeners without synchronizing on the 060 * state in case a listener takes a long time to complete. 061 */ 062 063 /* 064 * FIXME: This class should be used by connection pool and ldap connection 065 * implementations as well. 066 */ 067 068 /** 069 * Use the State design pattern to manage state transitions. 070 */ 071 private enum State { 072 073 /** 074 * Connection has not encountered an error nor has it been closed 075 * (initial state). 076 */ 077 VALID() { 078 @Override 079 void addConnectionEventListener(final ConnectionState cs, 080 final ConnectionEventListener listener) { 081 cs.listeners.add(listener); 082 } 083 084 @Override 085 boolean isClosed() { 086 return false; 087 } 088 089 @Override 090 boolean isFailed() { 091 return false; 092 } 093 094 @Override 095 boolean isValid() { 096 return true; 097 } 098 099 @Override 100 boolean notifyConnectionClosed(final ConnectionState cs) { 101 cs.state = CLOSED; 102 for (final ConnectionEventListener listener : cs.listeners) { 103 listener.handleConnectionClosed(); 104 } 105 return true; 106 } 107 108 @Override 109 boolean notifyConnectionError(final ConnectionState cs, 110 final boolean isDisconnectNotification, final LdapException error) { 111 // Transition from valid to error state. 112 cs.failedDueToDisconnect = isDisconnectNotification; 113 cs.connectionError = error; 114 cs.state = ERROR; 115 /* 116 * FIXME: a re-entrant close will invoke close listeners before 117 * error notification has completed. 118 */ 119 for (final ConnectionEventListener listener : cs.listeners) { 120 // Use the reason provided in the disconnect notification. 121 listener.handleConnectionError(isDisconnectNotification, error); 122 } 123 return true; 124 } 125 126 @Override 127 void notifyUnsolicitedNotification(final ConnectionState cs, 128 final ExtendedResult notification) { 129 for (final ConnectionEventListener listener : cs.listeners) { 130 listener.handleUnsolicitedNotification(notification); 131 } 132 } 133 }, 134 135 /** 136 * Connection has encountered an error, but has not been closed. 137 */ 138 ERROR() { 139 @Override 140 void addConnectionEventListener(final ConnectionState cs, 141 final ConnectionEventListener listener) { 142 listener.handleConnectionError(cs.failedDueToDisconnect, cs.connectionError); 143 cs.listeners.add(listener); 144 } 145 146 @Override 147 boolean isClosed() { 148 return false; 149 } 150 151 @Override 152 boolean isFailed() { 153 return true; 154 } 155 156 @Override 157 boolean isValid() { 158 return false; 159 } 160 161 @Override 162 boolean notifyConnectionClosed(final ConnectionState cs) { 163 cs.state = ERROR_CLOSED; 164 for (final ConnectionEventListener listener : cs.listeners) { 165 listener.handleConnectionClosed(); 166 } 167 return true; 168 } 169 }, 170 171 /** 172 * Connection has been closed (terminal state). 173 */ 174 CLOSED() { 175 @Override 176 void addConnectionEventListener(final ConnectionState cs, 177 final ConnectionEventListener listener) { 178 listener.handleConnectionClosed(); 179 } 180 181 @Override 182 boolean isClosed() { 183 return true; 184 } 185 186 @Override 187 boolean isFailed() { 188 return false; 189 } 190 191 @Override 192 boolean isValid() { 193 return false; 194 } 195 }, 196 197 /** 198 * Connection has encountered an error and has been closed (terminal 199 * state). 200 */ 201 ERROR_CLOSED() { 202 @Override 203 void addConnectionEventListener(final ConnectionState cs, 204 final ConnectionEventListener listener) { 205 listener.handleConnectionError(cs.failedDueToDisconnect, cs.connectionError); 206 listener.handleConnectionClosed(); 207 } 208 209 @Override 210 boolean isClosed() { 211 return true; 212 } 213 214 @Override 215 boolean isFailed() { 216 return true; 217 } 218 219 @Override 220 boolean isValid() { 221 return false; 222 } 223 }; 224 225 abstract void addConnectionEventListener(ConnectionState cs, 226 final ConnectionEventListener listener); 227 228 abstract boolean isClosed(); 229 230 abstract boolean isFailed(); 231 232 abstract boolean isValid(); 233 234 boolean notifyConnectionClosed(final ConnectionState cs) { 235 return false; 236 } 237 238 boolean notifyConnectionError(final ConnectionState cs, 239 final boolean isDisconnectNotification, final LdapException error) { 240 return false; 241 } 242 243 void notifyUnsolicitedNotification(final ConnectionState cs, 244 final ExtendedResult notification) { 245 // Do nothing by default. 246 } 247 } 248 249 /** 250 * Non-{@code null} once the connection has failed due to a connection 251 * error. Volatile so that it can be read without synchronization. 252 */ 253 private volatile LdapException connectionError; 254 255 /** Whether the connection has failed due to a disconnect notification. */ 256 private boolean failedDueToDisconnect; 257 258 /** Registered event listeners. */ 259 private final List<ConnectionEventListener> listeners = new LinkedList<>(); 260 261 /** Internal state implementation. */ 262 private volatile State state = State.VALID; 263 264 /** Creates a new connection state which is initially valid. */ 265 public ConnectionState() { 266 // Nothing to do. 267 } 268 269 /** 270 * Registers the provided connection event listener so that it will be 271 * notified when this connection is closed by the application, receives an 272 * unsolicited notification, or experiences a fatal error. 273 * 274 * @param listener 275 * The listener which wants to be notified when events occur on 276 * this connection. 277 * @throws IllegalStateException 278 * If this connection has already been closed, i.e. if 279 * {@code isClosed() == true}. 280 * @throws NullPointerException 281 * If the {@code listener} was {@code null}. 282 */ 283 public synchronized void addConnectionEventListener(final ConnectionEventListener listener) { 284 state.addConnectionEventListener(this, listener); 285 } 286 287 /** 288 * Returns the error that caused the connection to fail, or {@code null} if 289 * the connection has not failed. 290 * 291 * @return The error that caused the connection to fail, or {@code null} if 292 * the connection has not failed. 293 */ 294 public LdapException getConnectionError() { 295 return connectionError; 296 } 297 298 /** 299 * Indicates whether or not this connection has been explicitly closed by 300 * calling {@code close}. This method will not return {@code true} if a 301 * fatal error has occurred on the connection unless {@code close} has been 302 * called. 303 * 304 * @return {@code true} if this connection has been explicitly closed by 305 * calling {@code close}, or {@code false} otherwise. 306 */ 307 public boolean isClosed() { 308 return state.isClosed(); 309 } 310 311 /** 312 * Returns {@code true} if the associated connection has not been closed and 313 * no fatal errors have been detected. 314 * 315 * @return {@code true} if this connection is valid, {@code false} 316 * otherwise. 317 */ 318 public boolean isValid() { 319 return state.isValid(); 320 } 321 322 /** 323 * Attempts to transition this connection state to closed and invokes event 324 * listeners if successful. 325 * 326 * @return {@code true} if the state changed to closed, or {@code false} if 327 * the state was already closed. 328 * @see ConnectionEventListener#handleConnectionClosed() 329 */ 330 public synchronized boolean notifyConnectionClosed() { 331 return state.notifyConnectionClosed(this); 332 } 333 334 /** 335 * Attempts to transition this connection state to error and invokes event 336 * listeners if successful. 337 * 338 * @param isDisconnectNotification 339 * {@code true} if the error was triggered by a disconnect 340 * notification sent by the server, otherwise {@code false}. 341 * @param error 342 * The exception that is about to be thrown to the application. 343 * @return {@code true} if the state changed to error, or {@code false} if 344 * the state was already error or closed. 345 * @see ConnectionEventListener#handleConnectionError(boolean, 346 * LdapException) 347 */ 348 public synchronized boolean notifyConnectionError(final boolean isDisconnectNotification, 349 final LdapException error) { 350 return state.notifyConnectionError(this, isDisconnectNotification, error); 351 } 352 353 /** 354 * Notifies event listeners of the provided unsolicited notification if the 355 * state is valid. 356 * 357 * @param notification 358 * The unsolicited notification. 359 * @see ConnectionEventListener#handleUnsolicitedNotification(ExtendedResult) 360 */ 361 public synchronized void notifyUnsolicitedNotification(final ExtendedResult notification) { 362 state.notifyUnsolicitedNotification(this, notification); 363 } 364 365 /** 366 * Removes the provided connection event listener from this connection so 367 * that it will no longer be notified when this connection is closed by the 368 * application, receives an unsolicited notification, or experiences a fatal 369 * error. 370 * 371 * @param listener 372 * The listener which no longer wants to be notified when events 373 * occur on this connection. 374 * @throws NullPointerException 375 * If the {@code listener} was {@code null}. 376 */ 377 public synchronized void removeConnectionEventListener(final ConnectionEventListener listener) { 378 listeners.remove(listener); 379 } 380 381}