diff options
Diffstat (limited to 'src/org/jivesoftware/smackx/ping/PingManager.java')
-rw-r--r-- | src/org/jivesoftware/smackx/ping/PingManager.java | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/ping/PingManager.java b/src/org/jivesoftware/smackx/ping/PingManager.java new file mode 100644 index 0000000..6b4b48c --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/PingManager.java @@ -0,0 +1,343 @@ +/** + * Copyright 2012-2013 Florian Schmaus + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.ping.packet.Ping; +import org.jivesoftware.smackx.ping.packet.Pong; + +/** + * Implements the XMPP Ping as defined by XEP-0199. This protocol offers an + * alternative to the traditional 'white space ping' approach of determining the + * availability of an entity. The XMPP Ping protocol allows ping messages to be + * send in a more XML-friendly approach, which can be used over more than one + * hop in the communication path. + * + * @author Florian Schmaus + * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP + * Ping</a> + */ +public class PingManager { + + public static final String NAMESPACE = "urn:xmpp:ping"; + public static final String ELEMENT = "ping"; + + + private static Map<Connection, PingManager> instances = + Collections.synchronizedMap(new WeakHashMap<Connection, PingManager>()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new PingManager(connection); + } + }); + } + + private ScheduledExecutorService periodicPingExecutorService; + private Connection connection; + private int pingInterval = SmackConfiguration.getDefaultPingInterval(); + private Set<PingFailedListener> pingFailedListeners = Collections + .synchronizedSet(new HashSet<PingFailedListener>()); + private ScheduledFuture<?> periodicPingTask; + protected volatile long lastSuccessfulPingByTask = -1; + + + // Ping Flood protection + private long pingMinDelta = 100; + private long lastPingStamp = 0; // timestamp of the last received ping + + // Timestamp of the last pong received, either from the server or another entity + // Note, no need to synchronize this value, it will only increase over time + private long lastSuccessfulManualPing = -1; + + private PingManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(NAMESPACE); + this.connection = connection; + init(); + } + + private void init() { + periodicPingExecutorService = new ScheduledThreadPoolExecutor(1); + PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class); + connection.addPacketListener(new PacketListener() { + /** + * Sends a Pong for every Ping + */ + public void processPacket(Packet packet) { + if (pingMinDelta > 0) { + // Ping flood protection enabled + long currentMillies = System.currentTimeMillis(); + long delta = currentMillies - lastPingStamp; + lastPingStamp = currentMillies; + if (delta < pingMinDelta) { + return; + } + } + Pong pong = new Pong((Ping)packet); + connection.sendPacket(pong); + } + } + , pingPacketFilter); + connection.addConnectionListener(new ConnectionListener() { + + @Override + public void connectionClosed() { + maybeStopPingServerTask(); + } + + @Override + public void connectionClosedOnError(Exception arg0) { + maybeStopPingServerTask(); + } + + @Override + public void reconnectionSuccessful() { + maybeSchedulePingServerTask(); + } + + @Override + public void reconnectingIn(int seconds) { + } + + @Override + public void reconnectionFailed(Exception e) { + } + }); + instances.put(connection, this); + maybeSchedulePingServerTask(); + } + + public static PingManager getInstanceFor(Connection connection) { + PingManager pingManager = instances.get(connection); + + if (pingManager == null) { + pingManager = new PingManager(connection); + } + + return pingManager; + } + + public void setPingIntervall(int pingIntervall) { + this.pingInterval = pingIntervall; + } + + public int getPingIntervall() { + return pingInterval; + } + + public void registerPingFailedListener(PingFailedListener listener) { + pingFailedListeners.add(listener); + } + + public void unregisterPingFailedListener(PingFailedListener listener) { + pingFailedListeners.remove(listener); + } + + public void disablePingFloodProtection() { + setPingMinimumInterval(-1); + } + + public void setPingMinimumInterval(long ms) { + this.pingMinDelta = ms; + } + + public long getPingMinimumInterval() { + return this.pingMinDelta; + } + + /** + * Pings the given jid and returns the IQ response which is either of + * IQ.Type.ERROR or IQ.Type.RESULT. If we are not connected or if there was + * no reply, null is returned. + * + * You should use isPingSupported(jid) to determine if XMPP Ping is + * supported by the user. + * + * @param jid + * @param pingTimeout + * @return + */ + public IQ ping(String jid, long pingTimeout) { + // Make sure we actually connected to the server + if (!connection.isAuthenticated()) + return null; + + Ping ping = new Ping(connection.getUser(), jid); + + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(ping.getPacketID())); + + connection.sendPacket(ping); + + IQ result = (IQ) collector.nextResult(pingTimeout); + + collector.cancel(); + return result; + } + + /** + * Pings the given jid and returns the IQ response with the default + * packet reply timeout + * + * @param jid + * @return + */ + public IQ ping(String jid) { + return ping(jid, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Pings the given Entity. + * + * Note that XEP-199 shows that if we receive a error response + * service-unavailable there is no way to determine if the response was send + * by the entity (e.g. a user JID) or from a server in between. This is + * intended behavior to avoid presence leaks. + * + * Always use isPingSupported(jid) to determine if XMPP Ping is supported + * by the entity. + * + * @param jid + * @return True if a pong was received, otherwise false + */ + public boolean pingEntity(String jid, long pingTimeout) { + IQ result = ping(jid, pingTimeout); + + if (result == null || result.getType() == IQ.Type.ERROR) { + return false; + } + pongReceived(); + return true; + } + + public boolean pingEntity(String jid) { + return pingEntity(jid, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Pings the user's server. Will notify the registered + * pingFailedListeners in case of error. + * + * If we receive as response, we can be sure that it came from the server. + * + * @return true if successful, otherwise false + */ + public boolean pingMyServer(long pingTimeout) { + IQ result = ping(connection.getServiceName(), pingTimeout); + + if (result == null) { + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + return false; + } + // Maybe not really a pong, but an answer is an answer + pongReceived(); + return true; + } + + /** + * Pings the user's server with the PacketReplyTimeout as defined + * in SmackConfiguration. + * + * @return true if successful, otherwise false + */ + public boolean pingMyServer() { + return pingMyServer(SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Returns true if XMPP Ping is supported by a given JID + * + * @param jid + * @return + */ + public boolean isPingSupported(String jid) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); + return result.containsFeature(NAMESPACE); + } + catch (XMPPException e) { + return false; + } + } + + /** + * Returns the time of the last successful Ping Pong with the + * users server. If there was no successful Ping (e.g. because this + * feature is disabled) -1 will be returned. + * + * @return + */ + public long getLastSuccessfulPing() { + return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing); + } + + protected Set<PingFailedListener> getPingFailedListeners() { + return pingFailedListeners; + } + + /** + * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater + * then zero. + * + */ + protected synchronized void maybeSchedulePingServerTask() { + maybeStopPingServerTask(); + if (pingInterval > 0) { + periodicPingTask = periodicPingExecutorService.schedule(new ServerPingTask(connection), pingInterval, + TimeUnit.SECONDS); + } + } + + private void maybeStopPingServerTask() { + if (periodicPingTask != null) { + periodicPingTask.cancel(true); + periodicPingTask = null; + } + } + + private void pongReceived() { + lastSuccessfulManualPing = System.currentTimeMillis(); + } +} |