/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2013 ForgeRock AS. */ package com.forgerock.opendj.ldap; import java.util.LinkedList; import java.util.List; import org.forgerock.opendj.ldap.ConnectionEventListener; import org.forgerock.opendj.ldap.ErrorResultException; import org.forgerock.opendj.ldap.responses.ExtendedResult; /** * This class can be used to manage the internal state of a connection, ensuring * valid and atomic state transitions, as well as connection event listener * notification. There are 4 states: * * All methods are synchronized and container classes may also synchronize on * the state where needed. The state transition methods, * {@link #notifyConnectionClosed()} and * {@link #notifyConnectionError(boolean, ErrorResultException)}, correspond to * methods in the {@link ConnectionEventListener} interface except that they * return a boolean indicating whether the transition was successful or not. */ public final class ConnectionState { /* * FIXME: The synchronization in this class has been kept simple for now. * However, ideally we should notify listeners without synchronizing on the * state in case a listener takes a long time to complete. */ /* * FIXME: This class should be used by connection pool and ldap connection * implementations as well. */ /** * Use the State design pattern to manage state transitions. */ private enum State { /** * Connection has not encountered an error nor has it been closed * (initial state). */ VALID() { @Override void addConnectionEventListener(final ConnectionState cs, final ConnectionEventListener listener) { cs.listeners.add(listener); } @Override boolean isClosed() { return false; } @Override boolean isFailed() { return false; } @Override boolean isValid() { return true; } @Override boolean notifyConnectionClosed(final ConnectionState cs) { for (final ConnectionEventListener listener : cs.listeners) { listener.handleConnectionClosed(); } cs.state = CLOSED; return true; } @Override boolean notifyConnectionError(final ConnectionState cs, final boolean isDisconnectNotification, final ErrorResultException error) { // Transition from valid to error state. cs.failedDueToDisconnect = isDisconnectNotification; cs.connectionError = error; for (final ConnectionEventListener listener : cs.listeners) { // Use the reason provided in the disconnect notification. listener.handleConnectionError(isDisconnectNotification, error); } cs.state = ERROR; return true; } @Override void notifyUnsolicitedNotification(final ConnectionState cs, final ExtendedResult notification) { for (final ConnectionEventListener listener : cs.listeners) { listener.handleUnsolicitedNotification(notification); } } }, /** * Connection has encountered an error, but has not been closed. */ ERROR() { @Override void addConnectionEventListener(final ConnectionState cs, final ConnectionEventListener listener) { listener.handleConnectionError(cs.failedDueToDisconnect, cs.connectionError); cs.listeners.add(listener); } @Override boolean isClosed() { return false; } @Override boolean isFailed() { return true; } @Override boolean isValid() { return false; } @Override boolean notifyConnectionClosed(final ConnectionState cs) { for (final ConnectionEventListener listener : cs.listeners) { listener.handleConnectionClosed(); } cs.state = ERROR_CLOSED; return true; } }, /** * Connection has been closed (terminal state). */ CLOSED() { @Override void addConnectionEventListener(final ConnectionState cs, final ConnectionEventListener listener) { listener.handleConnectionClosed(); } @Override boolean isClosed() { return true; } @Override boolean isFailed() { return false; } @Override boolean isValid() { return false; } }, /** * Connection has encountered an error and has been closed (terminal * state). */ ERROR_CLOSED() { @Override void addConnectionEventListener(final ConnectionState cs, final ConnectionEventListener listener) { listener.handleConnectionError(cs.failedDueToDisconnect, cs.connectionError); listener.handleConnectionClosed(); } @Override boolean isClosed() { return true; } @Override boolean isFailed() { return true; } @Override boolean isValid() { return false; } }; abstract void addConnectionEventListener(ConnectionState cs, final ConnectionEventListener listener); abstract boolean isClosed(); abstract boolean isFailed(); abstract boolean isValid(); boolean notifyConnectionClosed(final ConnectionState cs) { return false; } boolean notifyConnectionError(final ConnectionState cs, final boolean isDisconnectNotification, final ErrorResultException error) { return false; } void notifyUnsolicitedNotification(final ConnectionState cs, final ExtendedResult notification) { // Do nothing by default. } } /** * Non-{@code null} once the connection has failed due to a connection * error. Volatile so that it can be read without synchronization. */ private volatile ErrorResultException connectionError = null; /** * {@code true} if the connection has failed due to a disconnect * notification. */ private boolean failedDueToDisconnect = false; /** * Registered event listeners. */ private final List listeners = new LinkedList(); /** * Internal state implementation. */ private volatile State state = State.VALID; /** * Creates a new connection state which is initially valid. */ public ConnectionState() { // Nothing to do. } /** * Registers the provided connection event listener so that it will be * notified when this connection is closed by the application, receives an * unsolicited notification, or experiences a fatal error. * * @param listener * The listener which wants to be notified when events occur on * this connection. * @throws IllegalStateException * If this connection has already been closed, i.e. if * {@code isClosed() == true}. * @throws NullPointerException * If the {@code listener} was {@code null}. */ public synchronized void addConnectionEventListener(final ConnectionEventListener listener) { state.addConnectionEventListener(this, listener); } /** * Returns the error that caused the connection to fail, or {@code null} if * the connection has not failed. * * @return The error that caused the connection to fail, or {@code null} if * the connection has not failed. */ public ErrorResultException getConnectionError() { return connectionError; } /** * Indicates whether or not this connection has been explicitly closed by * calling {@code close}. This method will not return {@code true} if a * fatal error has occurred on the connection unless {@code close} has been * called. * * @return {@code true} if this connection has been explicitly closed by * calling {@code close}, or {@code false} otherwise. */ public boolean isClosed() { return state.isClosed(); } /** * Returns {@code true} if the associated connection has not been closed and * no fatal errors have been detected. * * @return {@code true} if this connection is valid, {@code false} * otherwise. */ public boolean isValid() { return state.isValid(); } /** * Attempts to transition this connection state to closed and invokes event * listeners if successful. * * @return {@code true} if the state changed to closed, or {@code false} if * the state was already closed. * @see ConnectionEventListener#handleConnectionClosed() */ public synchronized boolean notifyConnectionClosed() { return state.notifyConnectionClosed(this); } /** * Attempts to transition this connection state to error and invokes event * listeners if successful. * * @param isDisconnectNotification * {@code true} if the error was triggered by a disconnect * notification sent by the server, otherwise {@code false}. * @param error * The exception that is about to be thrown to the application. * @return {@code true} if the state changed to error, or {@code false} if * the state was already error or closed. * @see ConnectionEventListener#handleConnectionError(boolean, * ErrorResultException) */ public synchronized boolean notifyConnectionError(final boolean isDisconnectNotification, final ErrorResultException error) { return state.notifyConnectionError(this, isDisconnectNotification, error); } /** * Notifies event listeners of the provided unsolicited notification if the * state is valid. * * @param notification * The unsolicited notification. * @see ConnectionEventListener#handleUnsolicitedNotification(ExtendedResult) */ public synchronized void notifyUnsolicitedNotification(final ExtendedResult notification) { state.notifyUnsolicitedNotification(this, notification); } /** * Removes the provided connection event listener from this connection so * that it will no longer be notified when this connection is closed by the * application, receives an unsolicited notification, or experiences a fatal * error. * * @param listener * The listener which no longer wants to be notified when events * occur on this connection. * @throws NullPointerException * If the {@code listener} was {@code null}. */ public synchronized void removeConnectionEventListener(final ConnectionEventListener listener) { listeners.remove(listener); } }