001/**
002 *
003 * Copyright 2012-2014 Florian Schmaus
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.smackx.ping;
018
019import java.util.Collections;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023import java.util.WeakHashMap;
024import java.util.concurrent.ScheduledFuture;
025import java.util.concurrent.TimeUnit;
026import java.util.logging.Level;
027import java.util.logging.Logger;
028
029import org.jivesoftware.smack.AbstractConnectionListener;
030import org.jivesoftware.smack.SmackException;
031import org.jivesoftware.smack.SmackException.NoResponseException;
032import org.jivesoftware.smack.SmackException.NotConnectedException;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.ConnectionCreationListener;
035import org.jivesoftware.smack.Manager;
036import org.jivesoftware.smack.PacketListener;
037import org.jivesoftware.smack.XMPPException;
038import org.jivesoftware.smack.XMPPException.XMPPErrorException;
039import org.jivesoftware.smack.filter.AndFilter;
040import org.jivesoftware.smack.filter.IQTypeFilter;
041import org.jivesoftware.smack.filter.PacketFilter;
042import org.jivesoftware.smack.filter.PacketTypeFilter;
043import org.jivesoftware.smack.packet.Packet;
044import org.jivesoftware.smack.packet.IQ.Type;
045import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
046import org.jivesoftware.smackx.ping.packet.Ping;
047import org.jivesoftware.smackx.ping.packet.Pong;
048
049/**
050 * Implements the XMPP Ping as defined by XEP-0199. The XMPP Ping protocol allows one entity to
051 * ping any other entity by simply sending a ping to the appropriate JID. PingManger also
052 * periodically sends XMPP pings to the server every 30 minutes to avoid NAT timeouts and to test
053 * the connection status.
054 * 
055 * @author Florian Schmaus
056 * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP Ping</a>
057 */
058public class PingManager extends Manager {
059    public static final String NAMESPACE = "urn:xmpp:ping";
060
061    private static final Logger LOGGER = Logger.getLogger(PingManager.class.getName());
062
063    private static final Map<XMPPConnection, PingManager> INSTANCES = Collections
064            .synchronizedMap(new WeakHashMap<XMPPConnection, PingManager>());
065
066    private static final PacketFilter PING_PACKET_FILTER = new AndFilter(
067                    new PacketTypeFilter(Ping.class), new IQTypeFilter(Type.GET));
068    private static final PacketFilter PONG_PACKET_FILTER = new AndFilter(new PacketTypeFilter(
069                    Pong.class), new IQTypeFilter(Type.RESULT));
070
071    static {
072        XMPPConnection.addConnectionCreationListener(new ConnectionCreationListener() {
073            public void connectionCreated(XMPPConnection connection) {
074                getInstanceFor(connection);
075            }
076        });
077    }
078
079    /**
080     * Retrieves a {@link PingManager} for the specified {@link XMPPConnection}, creating one if it doesn't already
081     * exist.
082     * 
083     * @param connection
084     * The connection the manager is attached to.
085     * @return The new or existing manager.
086     */
087    public synchronized static PingManager getInstanceFor(XMPPConnection connection) {
088        PingManager pingManager = INSTANCES.get(connection);
089        if (pingManager == null) {
090            pingManager = new PingManager(connection);
091        }
092        return pingManager;
093    }
094
095    private static int defaultPingInterval = 60 * 30;
096
097    /**
098     * Set the default ping interval which will be used for new connections.
099     *
100     * @param interval the interval in seconds
101     */
102    public static void setDefaultPingInterval(int interval) {
103        defaultPingInterval = interval;
104    }
105
106    private final Set<PingFailedListener> pingFailedListeners = Collections
107                    .synchronizedSet(new HashSet<PingFailedListener>());
108
109    /**
110     * The interval in seconds between pings are send to the users server.
111     */
112    private int pingInterval = defaultPingInterval;
113
114    private ScheduledFuture<?> nextAutomaticPing;
115
116    /**
117     * The time in milliseconds the last pong was received.
118     */
119    private long lastPongReceived = -1;
120
121    private PingManager(XMPPConnection connection) {
122        super(connection);
123        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
124        sdm.addFeature(PingManager.NAMESPACE);
125        INSTANCES.put(connection, this);
126
127        connection.addPacketListener(new PacketListener() {
128            // Send a Pong for every Ping
129            @Override
130            public void processPacket(Packet packet) throws NotConnectedException {
131                Pong pong = new Pong(packet);
132                connection().sendPacket(pong);
133            }
134        }, PING_PACKET_FILTER);
135        connection.addPacketListener(new PacketListener() {
136            @Override
137            public void processPacket(Packet packet) throws NotConnectedException {
138                lastPongReceived = System.currentTimeMillis();
139            }
140        }, PONG_PACKET_FILTER);
141        connection.addConnectionListener(new AbstractConnectionListener() {
142            @Override
143            public void authenticated(XMPPConnection connection) {
144                maybeSchedulePingServerTask();
145            }
146            @Override
147            public void connectionClosed() {
148                maybeStopPingServerTask();
149            }
150            @Override
151            public void connectionClosedOnError(Exception arg0) {
152                maybeStopPingServerTask();
153            }
154        });
155        maybeSchedulePingServerTask();
156    }
157
158    /**
159     * Pings the given jid. This method will return false if an error occurs.  The exception 
160     * to this, is a server ping, which will always return true if the server is reachable, 
161     * event if there is an error on the ping itself (i.e. ping not supported).
162     * <p>
163     * Use {@link #isPingSupported(String)} to determine if XMPP Ping is supported 
164     * by the entity.
165     * 
166     * @param jid The id of the entity the ping is being sent to
167     * @param pingTimeout The time to wait for a reply in milliseconds
168     * @return true if a reply was received from the entity, false otherwise.
169     * @throws NoResponseException if there was no response from the jid.
170     * @throws NotConnectedException 
171     */
172    public boolean ping(String jid, long pingTimeout) throws NotConnectedException, NoResponseException {
173        Ping ping = new Ping(jid);
174        try {
175            connection().createPacketCollectorAndSend(ping).nextResultOrThrow(pingTimeout);
176        }
177        catch (XMPPException exc) {
178            return jid.equals(connection().getServiceName());
179        }
180        return true;
181    }
182
183    /**
184     * Same as calling {@link #ping(String, long)} with the defaultpacket reply 
185     * timeout.
186     * 
187     * @param jid The id of the entity the ping is being sent to
188     * @return true if a reply was received from the entity, false otherwise.
189     * @throws NotConnectedException
190     * @throws NoResponseException if there was no response from the jid.
191     */
192    public boolean ping(String jid) throws NotConnectedException, NoResponseException {
193        return ping(jid, connection().getPacketReplyTimeout());
194    }
195
196    /**
197     * Query the specified entity to see if it supports the Ping protocol (XEP-0199)
198     * 
199     * @param jid The id of the entity the query is being sent to
200     * @return true if it supports ping, false otherwise.
201     * @throws XMPPErrorException An XMPP related error occurred during the request 
202     * @throws NoResponseException if there was no response from the jid.
203     * @throws NotConnectedException 
204     */
205    public boolean isPingSupported(String jid) throws NoResponseException, XMPPErrorException, NotConnectedException  {
206        return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, PingManager.NAMESPACE);
207    }
208
209    /**
210     * Pings the server. This method will return true if the server is reachable.  It
211     * is the equivalent of calling <code>ping</code> with the XMPP domain.
212     * <p>
213     * Unlike the {@link #ping(String)} case, this method will return true even if 
214     * {@link #isPingSupported(String)} is false.
215     * 
216     * @return true if a reply was received from the server, false otherwise.
217     * @throws NotConnectedException
218     */
219    public boolean pingMyServer() throws NotConnectedException {
220        return pingMyServer(true);
221    }
222
223    /**
224     * Pings the server. This method will return true if the server is reachable.  It
225     * is the equivalent of calling <code>ping</code> with the XMPP domain.
226     * <p>
227     * Unlike the {@link #ping(String)} case, this method will return true even if
228     * {@link #isPingSupported(String)} is false.
229     *
230     * @param notifyListeners Notify the PingFailedListener in case of error if true
231     * @return true if the user's server could be pinged.
232     * @throws NotConnectedException
233     */
234    public boolean pingMyServer(boolean notifyListeners) throws NotConnectedException {
235        boolean res;
236        try {
237            res = ping(connection().getServiceName());
238        }
239        catch (NoResponseException e) {
240            res = false;
241        }
242        if (!res && notifyListeners) {
243            for (PingFailedListener l : pingFailedListeners)
244                l.pingFailed();
245        }
246        return res;
247    }
248
249    /**
250     * Set the interval between the server is automatic pinged. A negative value disables automatic server pings.
251     *
252     * @param pingInterval the interval between the ping
253     */
254    public void setPingInterval(int pingInterval) {
255        this.pingInterval = pingInterval;
256        maybeSchedulePingServerTask();
257    }
258
259    /**
260     * Get the current ping interval.
261     *
262     * @return the interval between pings in seconds
263     */
264    public int getPingInterval() {
265        return pingInterval;
266    }
267
268    /**
269     * Register a new PingFailedListener
270     *
271     * @param listener the listener to invoke
272     */
273    public void registerPingFailedListener(PingFailedListener listener) {
274        pingFailedListeners.add(listener);
275    }
276
277    /**
278     * Unregister a PingFailedListener
279     *
280     * @param listener the listener to remove
281     */
282    public void unregisterPingFailedListener(PingFailedListener listener) {
283        pingFailedListeners.remove(listener);
284    }
285
286    /**
287     * Returns the timestamp when the last XMPP Pong was received.
288     * 
289     * @return the timestamp of the last XMPP Pong
290     */
291    public long getLastReceivedPong() {
292        return lastPongReceived;
293    }
294
295    private void maybeSchedulePingServerTask() {
296        maybeSchedulePingServerTask(0);
297    }
298
299    /**
300     * Cancels any existing periodic ping task if there is one and schedules a new ping task if
301     * pingInterval is greater then zero.
302     *
303     * @param delta the delta to the last received ping in seconds
304     */
305    private synchronized void maybeSchedulePingServerTask(int delta) {
306        maybeStopPingServerTask();
307        if (pingInterval > 0) {
308            int nextPingIn = pingInterval - delta;
309            LOGGER.fine("Scheduling ServerPingTask in " + nextPingIn + " seconds (pingInterval="
310                            + pingInterval + ", delta=" + delta + ")");
311            nextAutomaticPing = schedule(pingServerRunnable, nextPingIn, TimeUnit.SECONDS);
312        }
313    }
314
315    private void maybeStopPingServerTask() {
316        if (nextAutomaticPing != null) {
317            nextAutomaticPing.cancel(true);
318            nextAutomaticPing = null;
319        }
320    }
321
322    private final Runnable pingServerRunnable = new Runnable() {
323        private static final int DELTA = 1000; // 1 seconds
324        private static final int TRIES = 3; // 3 tries
325
326        public void run() {
327            LOGGER.fine("ServerPingTask run()");
328            XMPPConnection connection = connection();
329            if (connection == null) {
330                // connection has been collected by GC
331                // which means we can stop the thread by breaking the loop
332                return;
333            }
334            if (pingInterval <= 0) {
335                // Ping has been disabled
336                return;
337            }
338            long lastReceivedPong = getLastReceivedPong();
339            if (lastReceivedPong > 0) {
340                long now = System.currentTimeMillis();
341                // Calculate the delta from now to the next ping time. If delta is positive, the
342                // last successful ping was not to long ago, so we can defer the current ping.
343                int delta = (int) (((pingInterval * 1000) - (now - lastReceivedPong)) / 1000);
344                if (delta > 0) {
345                    maybeSchedulePingServerTask(delta);
346                    return;
347                }
348            }
349            if (connection.isAuthenticated()) {
350                boolean res = false;
351
352                for (int i = 0; i < TRIES; i++) {
353                    if (i != 0) {
354                        try {
355                            Thread.sleep(DELTA);
356                        } catch (InterruptedException e) {
357                            // We received an interrupt
358                            // This only happens if we should stop pinging
359                            return;
360                        }
361                    }
362                    try {
363                        res = pingMyServer(false);
364                    }
365                    catch (SmackException e) {
366                        LOGGER.log(Level.WARNING, "SmackError while pinging server", e);
367                        res = false;
368                    }
369                    // stop when we receive a pong back
370                    if (res) {
371                        break;
372                    }
373                }
374                LOGGER.fine("ServerPingTask res=" + res);
375                if (!res) {
376                    for (PingFailedListener l : pingFailedListeners) {
377                        l.pingFailed();
378                    }
379                } else {
380                    // Ping was successful, wind-up the periodic task again
381                    maybeSchedulePingServerTask();
382                }
383            } else {
384                LOGGER.warning("ServerPingTask: XMPPConnection was not authenticated");
385            }
386        }
387    };
388}