rocks.xmpp.extensions.ping.PingManager Maven / Gradle / Ivy
Show all versions of xmpp-extensions-client Show documentation
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Christian Schudt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package rocks.xmpp.extensions.ping;
import rocks.xmpp.addr.Jid;
import rocks.xmpp.core.XmppException;
import rocks.xmpp.core.session.Manager;
import rocks.xmpp.core.session.XmppSession;
import rocks.xmpp.core.stanza.AbstractIQHandler;
import rocks.xmpp.core.stanza.IQEvent;
import rocks.xmpp.core.stanza.IQHandler;
import rocks.xmpp.core.stanza.MessageEvent;
import rocks.xmpp.core.stanza.PresenceEvent;
import rocks.xmpp.core.stanza.StanzaException;
import rocks.xmpp.core.stanza.model.IQ;
import rocks.xmpp.core.stanza.model.errors.Condition;
import rocks.xmpp.extensions.ping.model.Ping;
import rocks.xmpp.util.XmppUtils;
import rocks.xmpp.util.concurrent.AsyncResult;
import java.time.Duration;
import java.util.concurrent.CompletionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* This class implements the application-level ping mechanism as specified in XEP-0199: XMPP Ping.
*
* If enabled, it periodically pings the server to ensure a stable connection. These pings are not sent as long as other stanzas are sent, because they serve the same purpose (telling the server, that we are still available).
*
* For Server-To-Client Pings it automatically responds with a result (pong), if enabled.
*
* It also allows to ping the server manually (Client-To-Server Pings) or to ping other XMPP entities (Client-to-Client Pings).
*
* @author Christian Schudt
*/
public final class PingManager extends Manager {
private final ScheduledExecutorService scheduledExecutorService;
/**
* guarded by "this"
*/
private ScheduledFuture> nextPing;
/**
* guarded by "this"
*/
private Duration pingInterval = Duration.ofMinutes(15);
private final IQHandler iqHandler;
private final Consumer inboundMessageListener;
private final Consumer inboundPresenceListener;
private final Consumer inboundIQListener;
/**
* Creates the ping manager.
*
* @param xmppSession The underlying XMPP session.
*/
private PingManager(final XmppSession xmppSession) {
super(xmppSession, true);
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(XmppUtils.createNamedThreadFactory("XMPP Scheduled Ping Thread"));
this.iqHandler = new AbstractIQHandler(IQ.Type.GET) {
@Override
protected IQ processRequest(IQ iq) {
return iq.createResult();
}
};
inboundMessageListener = e -> rescheduleNextPing();
inboundPresenceListener = e -> rescheduleNextPing();
inboundIQListener = e -> rescheduleNextPing();
}
@Override
protected final void onEnable() {
super.onEnable();
xmppSession.addIQHandler(Ping.class, iqHandler);
// Reschedule server pings whenever we receive a stanza from the server.
// When we receive a stanza, we are obviously connected.
// Pinging should be deferred in this case.
xmppSession.addInboundMessageListener(inboundMessageListener);
xmppSession.addInboundPresenceListener(inboundPresenceListener);
xmppSession.addInboundIQListener(inboundIQListener);
rescheduleNextPing();
}
@Override
protected final void onDisable() {
super.onDisable();
xmppSession.removeIQHandler(Ping.class);
xmppSession.removeInboundMessageListener(inboundMessageListener);
xmppSession.removeInboundPresenceListener(inboundPresenceListener);
xmppSession.removeInboundIQListener(inboundIQListener);
cancelNextPing();
}
/**
* Pings the given XMPP entity.
*
* @param jid The JID to ping.
* @return The async result with true if a response has been received within the timeout and the recipient is available, false otherwise.
*/
public final AsyncResult ping(Jid jid) {
IQ request = IQ.get(jid, Ping.INSTANCE);
return xmppSession.query(request).handle((iq, e) -> {
if (e != null) {
Throwable cause = e instanceof CompletionException ? e.getCause() : e;
if (cause instanceof RuntimeException) {
// Rethrow any RuntimeException, mainly CancellationException
throw (RuntimeException) e;
}
// If we pinged a full JID and the resource if offline, the server will respond on behalf of the user with .
// In this case we want to return false, because the intended recipient is unavailable.
// If we pinged a bare JID, the server will respond. If it returned a error, it just means it doesn't understand the ping protocol.
// Nonetheless an error response is still a valid pong, hence always return true in this case.
// If any other error is returned, most likely , , return false.
return cause instanceof StanzaException && (jid == null || jid.isBareJid()) && ((StanzaException) cause).getCondition() == Condition.SERVICE_UNAVAILABLE;
}
return true;
});
}
/**
* Pings the connected server.
*
* @return The async result with true if a response has been received, false otherwise.
*/
public final AsyncResult pingServer() {
return ping(xmppSession.getDomain());
}
/**
* Sets the automatic ping interval in seconds. Any scheduled future ping is canceled and a new ping is scheduled after the specified interval.
*
* @param pingInterval The ping interval in seconds.
* @see #getPingInterval()
* @deprecated Use {@link #setPingInterval(Duration)}
*/
@Deprecated
public final synchronized void setPingInterval(long pingInterval) {
setPingInterval(Duration.ofSeconds(pingInterval));
}
/**
* Gets the ping interval. The default ping interval is 900 seconds (15 minutes).
*
* @return The ping interval in seconds.
* @see #setPingInterval(long)
*/
public final synchronized Duration getPingInterval() {
return pingInterval;
}
/**
* Sets the automatic ping interval. Any scheduled future ping is canceled and a new ping is scheduled after the specified interval.
*
* @param pingInterval The ping interval in seconds.
* @see #getPingInterval()
*/
public final synchronized void setPingInterval(Duration pingInterval) {
this.pingInterval = pingInterval;
rescheduleNextPing();
}
private synchronized void rescheduleNextPing() {
// Reschedule in a separate thread, so that it won't interrupt the "pinging" thread due to the cancel, which then causes the ping to fail.
if (pingInterval != null && !pingInterval.isNegative() && !scheduledExecutorService.isShutdown()) {
cancelNextPing();
nextPing = scheduledExecutorService.schedule(() -> {
if (isEnabled() && xmppSession.getStatus() == XmppSession.Status.AUTHENTICATED) {
pingServer().thenAccept(result -> {
if (!result) {
try {
throw new XmppException("Server ping failed.");
} catch (XmppException e) {
xmppSession.notifyException(e);
}
}
});
}
// Rescheduling of the next ping is already done by the IQ response of the ping.
}, pingInterval.getSeconds(), TimeUnit.SECONDS);
}
}
/**
* If a ping has been scheduled, it will be canceled.
*/
private synchronized void cancelNextPing() {
if (nextPing != null) {
nextPing.cancel(false);
nextPing = null;
}
}
@Override
protected void dispose() {
// Shutdown the ping executor service and cancel the next ping.
synchronized (this) {
cancelNextPing();
scheduledExecutorService.shutdown();
}
}
}