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}