blob: 6b4b48c42a2a89dcb73bfe92167d089615dcbd4b [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
2 * Copyright 2012-2013 Florian Schmaus
3 *
4 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.jivesoftware.smackx.ping;
18
19import java.util.Collections;
20import java.util.HashSet;
21import java.util.Map;
22import java.util.Set;
23import java.util.WeakHashMap;
24import java.util.concurrent.ScheduledExecutorService;
25import java.util.concurrent.ScheduledFuture;
26import java.util.concurrent.ScheduledThreadPoolExecutor;
27import java.util.concurrent.TimeUnit;
28
29import org.jivesoftware.smack.Connection;
30import org.jivesoftware.smack.ConnectionCreationListener;
31import org.jivesoftware.smack.ConnectionListener;
32import org.jivesoftware.smack.PacketCollector;
33import org.jivesoftware.smack.PacketListener;
34import org.jivesoftware.smack.SmackConfiguration;
35import org.jivesoftware.smack.XMPPException;
36import org.jivesoftware.smack.filter.PacketFilter;
37import org.jivesoftware.smack.filter.PacketIDFilter;
38import org.jivesoftware.smack.filter.PacketTypeFilter;
39import org.jivesoftware.smack.packet.IQ;
40import org.jivesoftware.smack.packet.Packet;
41import org.jivesoftware.smackx.ServiceDiscoveryManager;
42import org.jivesoftware.smackx.packet.DiscoverInfo;
43import org.jivesoftware.smackx.ping.packet.Ping;
44import org.jivesoftware.smackx.ping.packet.Pong;
45
46/**
47 * Implements the XMPP Ping as defined by XEP-0199. This protocol offers an
48 * alternative to the traditional 'white space ping' approach of determining the
49 * availability of an entity. The XMPP Ping protocol allows ping messages to be
50 * send in a more XML-friendly approach, which can be used over more than one
51 * hop in the communication path.
52 *
53 * @author Florian Schmaus
54 * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP
55 * Ping</a>
56 */
57public class PingManager {
58
59 public static final String NAMESPACE = "urn:xmpp:ping";
60 public static final String ELEMENT = "ping";
61
62
63 private static Map<Connection, PingManager> instances =
64 Collections.synchronizedMap(new WeakHashMap<Connection, PingManager>());
65
66 static {
67 Connection.addConnectionCreationListener(new ConnectionCreationListener() {
68 public void connectionCreated(Connection connection) {
69 new PingManager(connection);
70 }
71 });
72 }
73
74 private ScheduledExecutorService periodicPingExecutorService;
75 private Connection connection;
76 private int pingInterval = SmackConfiguration.getDefaultPingInterval();
77 private Set<PingFailedListener> pingFailedListeners = Collections
78 .synchronizedSet(new HashSet<PingFailedListener>());
79 private ScheduledFuture<?> periodicPingTask;
80 protected volatile long lastSuccessfulPingByTask = -1;
81
82
83 // Ping Flood protection
84 private long pingMinDelta = 100;
85 private long lastPingStamp = 0; // timestamp of the last received ping
86
87 // Timestamp of the last pong received, either from the server or another entity
88 // Note, no need to synchronize this value, it will only increase over time
89 private long lastSuccessfulManualPing = -1;
90
91 private PingManager(Connection connection) {
92 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
93 sdm.addFeature(NAMESPACE);
94 this.connection = connection;
95 init();
96 }
97
98 private void init() {
99 periodicPingExecutorService = new ScheduledThreadPoolExecutor(1);
100 PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class);
101 connection.addPacketListener(new PacketListener() {
102 /**
103 * Sends a Pong for every Ping
104 */
105 public void processPacket(Packet packet) {
106 if (pingMinDelta > 0) {
107 // Ping flood protection enabled
108 long currentMillies = System.currentTimeMillis();
109 long delta = currentMillies - lastPingStamp;
110 lastPingStamp = currentMillies;
111 if (delta < pingMinDelta) {
112 return;
113 }
114 }
115 Pong pong = new Pong((Ping)packet);
116 connection.sendPacket(pong);
117 }
118 }
119 , pingPacketFilter);
120 connection.addConnectionListener(new ConnectionListener() {
121
122 @Override
123 public void connectionClosed() {
124 maybeStopPingServerTask();
125 }
126
127 @Override
128 public void connectionClosedOnError(Exception arg0) {
129 maybeStopPingServerTask();
130 }
131
132 @Override
133 public void reconnectionSuccessful() {
134 maybeSchedulePingServerTask();
135 }
136
137 @Override
138 public void reconnectingIn(int seconds) {
139 }
140
141 @Override
142 public void reconnectionFailed(Exception e) {
143 }
144 });
145 instances.put(connection, this);
146 maybeSchedulePingServerTask();
147 }
148
149 public static PingManager getInstanceFor(Connection connection) {
150 PingManager pingManager = instances.get(connection);
151
152 if (pingManager == null) {
153 pingManager = new PingManager(connection);
154 }
155
156 return pingManager;
157 }
158
159 public void setPingIntervall(int pingIntervall) {
160 this.pingInterval = pingIntervall;
161 }
162
163 public int getPingIntervall() {
164 return pingInterval;
165 }
166
167 public void registerPingFailedListener(PingFailedListener listener) {
168 pingFailedListeners.add(listener);
169 }
170
171 public void unregisterPingFailedListener(PingFailedListener listener) {
172 pingFailedListeners.remove(listener);
173 }
174
175 public void disablePingFloodProtection() {
176 setPingMinimumInterval(-1);
177 }
178
179 public void setPingMinimumInterval(long ms) {
180 this.pingMinDelta = ms;
181 }
182
183 public long getPingMinimumInterval() {
184 return this.pingMinDelta;
185 }
186
187 /**
188 * Pings the given jid and returns the IQ response which is either of
189 * IQ.Type.ERROR or IQ.Type.RESULT. If we are not connected or if there was
190 * no reply, null is returned.
191 *
192 * You should use isPingSupported(jid) to determine if XMPP Ping is
193 * supported by the user.
194 *
195 * @param jid
196 * @param pingTimeout
197 * @return
198 */
199 public IQ ping(String jid, long pingTimeout) {
200 // Make sure we actually connected to the server
201 if (!connection.isAuthenticated())
202 return null;
203
204 Ping ping = new Ping(connection.getUser(), jid);
205
206 PacketCollector collector =
207 connection.createPacketCollector(new PacketIDFilter(ping.getPacketID()));
208
209 connection.sendPacket(ping);
210
211 IQ result = (IQ) collector.nextResult(pingTimeout);
212
213 collector.cancel();
214 return result;
215 }
216
217 /**
218 * Pings the given jid and returns the IQ response with the default
219 * packet reply timeout
220 *
221 * @param jid
222 * @return
223 */
224 public IQ ping(String jid) {
225 return ping(jid, SmackConfiguration.getPacketReplyTimeout());
226 }
227
228 /**
229 * Pings the given Entity.
230 *
231 * Note that XEP-199 shows that if we receive a error response
232 * service-unavailable there is no way to determine if the response was send
233 * by the entity (e.g. a user JID) or from a server in between. This is
234 * intended behavior to avoid presence leaks.
235 *
236 * Always use isPingSupported(jid) to determine if XMPP Ping is supported
237 * by the entity.
238 *
239 * @param jid
240 * @return True if a pong was received, otherwise false
241 */
242 public boolean pingEntity(String jid, long pingTimeout) {
243 IQ result = ping(jid, pingTimeout);
244
245 if (result == null || result.getType() == IQ.Type.ERROR) {
246 return false;
247 }
248 pongReceived();
249 return true;
250 }
251
252 public boolean pingEntity(String jid) {
253 return pingEntity(jid, SmackConfiguration.getPacketReplyTimeout());
254 }
255
256 /**
257 * Pings the user's server. Will notify the registered
258 * pingFailedListeners in case of error.
259 *
260 * If we receive as response, we can be sure that it came from the server.
261 *
262 * @return true if successful, otherwise false
263 */
264 public boolean pingMyServer(long pingTimeout) {
265 IQ result = ping(connection.getServiceName(), pingTimeout);
266
267 if (result == null) {
268 for (PingFailedListener l : pingFailedListeners) {
269 l.pingFailed();
270 }
271 return false;
272 }
273 // Maybe not really a pong, but an answer is an answer
274 pongReceived();
275 return true;
276 }
277
278 /**
279 * Pings the user's server with the PacketReplyTimeout as defined
280 * in SmackConfiguration.
281 *
282 * @return true if successful, otherwise false
283 */
284 public boolean pingMyServer() {
285 return pingMyServer(SmackConfiguration.getPacketReplyTimeout());
286 }
287
288 /**
289 * Returns true if XMPP Ping is supported by a given JID
290 *
291 * @param jid
292 * @return
293 */
294 public boolean isPingSupported(String jid) {
295 try {
296 DiscoverInfo result =
297 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
298 return result.containsFeature(NAMESPACE);
299 }
300 catch (XMPPException e) {
301 return false;
302 }
303 }
304
305 /**
306 * Returns the time of the last successful Ping Pong with the
307 * users server. If there was no successful Ping (e.g. because this
308 * feature is disabled) -1 will be returned.
309 *
310 * @return
311 */
312 public long getLastSuccessfulPing() {
313 return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing);
314 }
315
316 protected Set<PingFailedListener> getPingFailedListeners() {
317 return pingFailedListeners;
318 }
319
320 /**
321 * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater
322 * then zero.
323 *
324 */
325 protected synchronized void maybeSchedulePingServerTask() {
326 maybeStopPingServerTask();
327 if (pingInterval > 0) {
328 periodicPingTask = periodicPingExecutorService.schedule(new ServerPingTask(connection), pingInterval,
329 TimeUnit.SECONDS);
330 }
331 }
332
333 private void maybeStopPingServerTask() {
334 if (periodicPingTask != null) {
335 periodicPingTask.cancel(true);
336 periodicPingTask = null;
337 }
338 }
339
340 private void pongReceived() {
341 lastSuccessfulManualPing = System.currentTimeMillis();
342 }
343}