001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack;
018
019import org.jivesoftware.smack.XMPPException.StreamErrorException;
020import org.jivesoftware.smack.packet.StreamError;
021
022import java.util.Random;
023import java.util.logging.Logger;
024/**
025 * Handles the automatic reconnection process. Every time a connection is dropped without
026 * the application explicitly closing it, the manager automatically tries to reconnect to
027 * the server.<p>
028 *
029 * The reconnection mechanism will try to reconnect periodically:
030 * <ol>
031 *  <li>For the first minute it will attempt to connect once every ten seconds.
032 *  <li>For the next five minutes it will attempt to connect once a minute.
033 *  <li>If that fails it will indefinitely try to connect once every five minutes.
034 * </ol>
035 *
036 * @author Francisco Vives
037 */
038public class ReconnectionManager extends AbstractConnectionListener {
039    private static final Logger LOGGER = Logger.getLogger(ReconnectionManager.class.getName());
040    
041    // Holds the connection to the server
042    private XMPPConnection connection;
043    private Thread reconnectionThread;
044    private int randomBase = new Random().nextInt(11) + 5; // between 5 and 15 seconds
045    
046    // Holds the state of the reconnection
047    boolean done = false;
048
049    static {
050        // Create a new PrivacyListManager on every established connection. In the init()
051        // method of PrivacyListManager, we'll add a listener that will delete the
052        // instance when the connection is closed.
053        XMPPConnection.addConnectionCreationListener(new ConnectionCreationListener() {
054            public void connectionCreated(XMPPConnection connection) {
055                connection.addConnectionListener(new ReconnectionManager(connection));
056            }
057        });
058    }
059
060    private ReconnectionManager(XMPPConnection connection) {
061        this.connection = connection;
062    }
063
064
065    /**
066     * Returns true if the reconnection mechanism is enabled.
067     *
068     * @return true if automatic reconnections are allowed.
069     */
070    private boolean isReconnectionAllowed() {
071        return !done && !connection.isConnected()
072                && connection.getConfiguration().isReconnectionAllowed();
073    }
074
075    /**
076     * Starts a reconnection mechanism if it was configured to do that.
077     * The algorithm is been executed when the first connection error is detected.
078     * <p/>
079     * The reconnection mechanism will try to reconnect periodically in this way:
080     * <ol>
081     * <li>First it will try 6 times every 10 seconds.
082     * <li>Then it will try 10 times every 1 minute.
083     * <li>Finally it will try indefinitely every 5 minutes.
084     * </ol>
085     */
086    synchronized protected void reconnect() {
087        if (this.isReconnectionAllowed()) {
088            // Since there is no thread running, creates a new one to attempt
089            // the reconnection.
090            // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
091            if (reconnectionThread!=null && reconnectionThread.isAlive()) return;
092            
093            reconnectionThread = new Thread() {
094                                
095                /**
096                 * Holds the current number of reconnection attempts
097                 */
098                private int attempts = 0;
099
100                /**
101                 * Returns the number of seconds until the next reconnection attempt.
102                 *
103                 * @return the number of seconds until the next reconnection attempt.
104                 */
105                private int timeDelay() {
106                    attempts++;
107                    if (attempts > 13) {
108                        return randomBase*6*5;      // between 2.5 and 7.5 minutes (~5 minutes)
109                    }
110                    if (attempts > 7) {
111                        return randomBase*6;       // between 30 and 90 seconds (~1 minutes)
112                    }
113                    return randomBase;       // 10 seconds
114                }
115
116                /**
117                 * The process will try the reconnection until the connection succeed or the user
118                 * cancel it
119                 */
120                public void run() {
121                    // The process will try to reconnect until the connection is established or
122                    // the user cancel the reconnection process {@link XMPPConnection#disconnect()}
123                    while (ReconnectionManager.this.isReconnectionAllowed()) {
124                        // Find how much time we should wait until the next reconnection
125                        int remainingSeconds = timeDelay();
126                        // Sleep until we're ready for the next reconnection attempt. Notify
127                        // listeners once per second about how much time remains before the next
128                        // reconnection attempt.
129                        while (ReconnectionManager.this.isReconnectionAllowed() &&
130                                remainingSeconds > 0)
131                        {
132                            try {
133                                Thread.sleep(1000);
134                                remainingSeconds--;
135                                ReconnectionManager.this
136                                        .notifyAttemptToReconnectIn(remainingSeconds);
137                            }
138                            catch (InterruptedException e1) {
139                                LOGGER.warning("Sleeping thread interrupted");
140                                // Notify the reconnection has failed
141                                ReconnectionManager.this.notifyReconnectionFailed(e1);
142                            }
143                        }
144
145                        // Makes a reconnection attempt
146                        try {
147                            if (ReconnectionManager.this.isReconnectionAllowed()) {
148                                connection.connect();
149                            }
150                        }
151                        catch (Exception e) {
152                            // Fires the failed reconnection notification
153                            ReconnectionManager.this.notifyReconnectionFailed(e);
154                        }
155                    }
156                }
157            };
158            reconnectionThread.setName("Smack Reconnection Manager");
159            reconnectionThread.setDaemon(true);
160            reconnectionThread.start();
161        }
162    }
163
164    /**
165     * Fires listeners when a reconnection attempt has failed.
166     *
167     * @param exception the exception that occured.
168     */
169    protected void notifyReconnectionFailed(Exception exception) {
170        if (isReconnectionAllowed()) {
171            for (ConnectionListener listener : connection.connectionListeners) {
172                listener.reconnectionFailed(exception);
173            }
174        }
175    }
176
177    /**
178     * Fires listeners when The XMPPConnection will retry a reconnection. Expressed in seconds.
179     *
180     * @param seconds the number of seconds that a reconnection will be attempted in.
181     */
182    protected void notifyAttemptToReconnectIn(int seconds) {
183        if (isReconnectionAllowed()) {
184            for (ConnectionListener listener : connection.connectionListeners) {
185                listener.reconnectingIn(seconds);
186            }
187        }
188    }
189
190    @Override
191    public void connectionClosed() {
192        done = true;
193    }
194
195    @Override
196    public void connectionClosedOnError(Exception e) {
197        done = false;
198        if (e instanceof StreamErrorException) {
199            StreamErrorException xmppEx = (StreamErrorException) e;
200            StreamError error = xmppEx.getStreamError();
201            String reason = error.getCode();
202
203            if ("conflict".equals(reason)) {
204                return;
205            }
206        }
207
208        if (this.isReconnectionAllowed()) {
209            this.reconnect();
210        }
211    }
212}