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 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(); 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}