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}