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