1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 *     http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18package org.jivesoftware.smack;
19
20import org.jivesoftware.smack.packet.StreamError;
21import java.util.Random;
22/**
23 * Handles the automatic reconnection process. Every time a connection is dropped without
24 * the application explictly closing it, the manager automatically tries to reconnect to
25 * the server.<p>
26 *
27 * The reconnection mechanism will try to reconnect periodically:
28 * <ol>
29 *  <li>For the first minute it will attempt to connect once every ten seconds.
30 *  <li>For the next five minutes it will attempt to connect once a minute.
31 *  <li>If that fails it will indefinitely try to connect once every five minutes.
32 * </ol>
33 *
34 * @author Francisco Vives
35 */
36public class ReconnectionManager implements ConnectionListener {
37
38    // Holds the connection to the server
39    private Connection connection;
40    private Thread reconnectionThread;
41    private int randomBase = new Random().nextInt(11) + 5; // between 5 and 15 seconds
42
43    // Holds the state of the reconnection
44    boolean done = false;
45
46    static {
47        // Create a new PrivacyListManager on every established connection. In the init()
48        // method of PrivacyListManager, we'll add a listener that will delete the
49        // instance when the connection is closed.
50        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
51            public void connectionCreated(Connection connection) {
52                connection.addConnectionListener(new ReconnectionManager(connection));
53            }
54        });
55    }
56
57    private ReconnectionManager(Connection connection) {
58        this.connection = connection;
59    }
60
61
62    /**
63     * Returns true if the reconnection mechanism is enabled.
64     *
65     * @return true if automatic reconnections are allowed.
66     */
67    private boolean isReconnectionAllowed() {
68        return !done && !connection.isConnected()
69                && connection.isReconnectionAllowed();
70    }
71
72    /**
73     * Starts a reconnection mechanism if it was configured to do that.
74     * The algorithm is been executed when the first connection error is detected.
75     * <p/>
76     * The reconnection mechanism will try to reconnect periodically in this way:
77     * <ol>
78     * <li>First it will try 6 times every 10 seconds.
79     * <li>Then it will try 10 times every 1 minute.
80     * <li>Finally it will try indefinitely every 5 minutes.
81     * </ol>
82     */
83    synchronized protected void reconnect() {
84        if (this.isReconnectionAllowed()) {
85            // Since there is no thread running, creates a new one to attempt
86            // the reconnection.
87            // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
88            if (reconnectionThread!=null && reconnectionThread.isAlive()) return;
89
90            reconnectionThread = new Thread() {
91
92                /**
93                 * Holds the current number of reconnection attempts
94                 */
95                private int attempts = 0;
96
97                /**
98                 * Returns the number of seconds until the next reconnection attempt.
99                 *
100                 * @return the number of seconds until the next reconnection attempt.
101                 */
102                private int timeDelay() {
103                    attempts++;
104                    if (attempts > 13) {
105                	return randomBase*6*5;      // between 2.5 and 7.5 minutes (~5 minutes)
106                    }
107                    if (attempts > 7) {
108                	return randomBase*6;       // between 30 and 90 seconds (~1 minutes)
109                    }
110                    return randomBase;       // 10 seconds
111                }
112
113                /**
114                 * The process will try the reconnection until the connection succeed or the user
115                 * cancell it
116                 */
117                public void run() {
118                    // The process will try to reconnect until the connection is established or
119                    // the user cancel the reconnection process {@link Connection#disconnect()}
120                    while (ReconnectionManager.this.isReconnectionAllowed()) {
121                        // Find how much time we should wait until the next reconnection
122                        int remainingSeconds = timeDelay();
123                        // Sleep until we're ready for the next reconnection attempt. Notify
124                        // listeners once per second about how much time remains before the next
125                        // reconnection attempt.
126                        while (ReconnectionManager.this.isReconnectionAllowed() &&
127                                remainingSeconds > 0)
128                        {
129                            try {
130                                Thread.sleep(1000);
131                                remainingSeconds--;
132                                ReconnectionManager.this
133                                        .notifyAttemptToReconnectIn(remainingSeconds);
134                            }
135                            catch (InterruptedException e1) {
136                                e1.printStackTrace();
137                                // Notify the reconnection has failed
138                                ReconnectionManager.this.notifyReconnectionFailed(e1);
139                            }
140                        }
141
142                        // Makes a reconnection attempt
143                        try {
144                            if (ReconnectionManager.this.isReconnectionAllowed()) {
145                                connection.connect();
146                            }
147                        }
148                        catch (XMPPException e) {
149                            // Fires the failed reconnection notification
150                            ReconnectionManager.this.notifyReconnectionFailed(e);
151                        }
152                    }
153                }
154            };
155            reconnectionThread.setName("Smack Reconnection Manager");
156            reconnectionThread.setDaemon(true);
157            reconnectionThread.start();
158        }
159    }
160
161    /**
162     * Fires listeners when a reconnection attempt has failed.
163     *
164     * @param exception the exception that occured.
165     */
166    protected void notifyReconnectionFailed(Exception exception) {
167        if (isReconnectionAllowed()) {
168            for (ConnectionListener listener : connection.connectionListeners) {
169                listener.reconnectionFailed(exception);
170            }
171        }
172    }
173
174    /**
175     * Fires listeners when The Connection will retry a reconnection. Expressed in seconds.
176     *
177     * @param seconds the number of seconds that a reconnection will be attempted in.
178     */
179    protected void notifyAttemptToReconnectIn(int seconds) {
180        if (isReconnectionAllowed()) {
181            for (ConnectionListener listener : connection.connectionListeners) {
182                listener.reconnectingIn(seconds);
183            }
184        }
185    }
186
187    public void connectionClosed() {
188        done = true;
189    }
190
191    public void connectionClosedOnError(Exception e) {
192        done = false;
193        if (e instanceof XMPPException) {
194            XMPPException xmppEx = (XMPPException) e;
195            StreamError error = xmppEx.getStreamError();
196
197            // Make sure the error is not null
198            if (error != null) {
199                String reason = error.getCode();
200
201                if ("conflict".equals(reason)) {
202                    return;
203                }
204            }
205        }
206
207        if (this.isReconnectionAllowed()) {
208            this.reconnect();
209        }
210    }
211
212    public void reconnectingIn(int seconds) {
213        // ignore
214    }
215
216    public void reconnectionFailed(Exception e) {
217        // ignore
218    }
219
220    /**
221     * The connection has successfull gotten connected.
222     */
223    public void reconnectionSuccessful() {
224        // ignore
225    }
226
227}