org.waveprotocol.wave.federation.xmpp.XmppManager.java Source code

Java tutorial

Introduction

Here is the source code for org.waveprotocol.wave.federation.xmpp.XmppManager.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.waveprotocol.wave.federation.xmpp;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.MapMaker;
import com.google.inject.Inject;
import com.google.inject.name.Named;

import org.dom4j.Element;
import org.waveprotocol.wave.federation.FederationErrorProto.FederationError;
import org.waveprotocol.wave.federation.FederationErrors;
import org.waveprotocol.wave.federation.FederationSettings;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;

import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Provides abstraction between Federation-specific code and the backing XMPP
 * transport, including support for reliable outgoing calls (i.e. calls that are
 * guaranteed to time out) and sending error responses.
 *
 * TODO(thorogood): Find a better name for this class. Suggestions include
 * PacketHandler, Switchbox, TransportConnector, ReliableRouter, ...
 *
 * @author thorogood@google.com (Sam Thorogood)
 */
public class XmppManager implements IncomingPacketHandler {
    private static final Logger LOG = Logger.getLogger(XmppManager.class.getCanonicalName());

    /**
     * Inner static class representing a single outgoing call.
     */
    private static class OutgoingCall {
        final Class<? extends Packet> responseType;
        PacketCallback callback;
        ScheduledFuture<?> timeout;

        OutgoingCall(Class<? extends Packet> responseType, PacketCallback callback) {
            this.responseType = responseType;
            this.callback = callback;
        }

        void start(ScheduledFuture<?> timeout) {
            Preconditions.checkState(this.timeout == null);
            this.timeout = timeout;
        }
    }

    /**
     * Inner non-static class representing a single incoming call. These are not
     * cancellable and do not time out; this is just a helper class so success and
     * failure responses may be more cleanly invoked.
     */
    private class IncomingCallback implements PacketCallback {
        private final Packet request;
        private boolean complete = false;

        IncomingCallback(Packet request) {
            this.request = request;
        }

        @Override
        public void error(FederationError error) {
            Preconditions.checkState(!complete, "Must not callback multiple times for incoming packet: %s",
                    request);
            complete = true;
            sendErrorResponse(request, error);
        }

        @Override
        public void run(Packet response) {
            Preconditions.checkState(!complete, "Must not callback multiple times for incoming packet: %s",
                    request);
            // TODO(thorogood): Check outgoing response versus stored incoming request
            // to ensure that to/from are paired correctly?
            complete = true;
            transport.sendPacket(response);
        }
    }

    // Injected types that handle incoming XMPP packet types.
    private final XmppFederationHost host;
    private final XmppFederationRemote remote;
    private final XmppDisco disco;
    private final OutgoingPacketTransport transport;
    private final String jid;

    // Pending callbacks to outgoing requests.
    private final ConcurrentMap<String, OutgoingCall> callbacks = new MapMaker().makeMap();
    private final ScheduledExecutorService timeoutExecutor = Executors.newSingleThreadScheduledExecutor();

    @Inject
    public XmppManager(XmppFederationHost host, XmppFederationRemote remote, XmppDisco disco,
            OutgoingPacketTransport transport, @Named(FederationSettings.XMPP_JID) String jid) {
        this.host = host;
        this.remote = remote;
        this.disco = disco;
        this.transport = transport;
        this.jid = jid;

        // Configure all related objects with this manager. Eventually, this should
        // be replaced by better Guice interface bindings.
        host.setManager(this);
        remote.setManager(this);
        disco.setManager(this);
    }

    @Override
    public void receivePacket(final Packet packet) {
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Received incoming XMPP packet:\n" + packet);
        }

        if (packet instanceof IQ) {
            IQ iq = (IQ) packet;
            if (iq.getType().equals(IQ.Type.result) || iq.getType().equals(IQ.Type.error)) {
                // Result type, hand off to callback handler.
                response(packet);
            } else {
                processIqGetSet(iq);
            }
        } else if (packet instanceof Message) {
            Message message = (Message) packet;
            if (message.getType().equals(Message.Type.error)
                    || message.getChildElement("received", XmppNamespace.NAMESPACE_XMPP_RECEIPTS) != null) {
                // Response type, hand off to callback handler.
                response(packet);
            } else {
                processMessage(message);
            }
        } else {
            sendErrorResponse(packet, FederationError.Code.BAD_REQUEST,
                    "Unhandled packet type: " + packet.getElement().getQName().getName());
        }
    }

    /**
     * Populate the given request subclass of Packet and return it.
     */
    private <V extends Packet> V createRequest(V packet, String toJid) {
        packet.setTo(toJid);
        packet.setID(XmppUtil.generateUniqueId());
        packet.setFrom(jid);
        return packet;
    }

    /**
     * Create a request IQ stanza with the given toJid.
     *
     * @param toJid target JID
     * @return new IQ stanza
     */
    public IQ createRequestIQ(String toJid) {
        return createRequest(new IQ(), toJid);
    }

    /**
     * Create a request Message stanza with the given toJid.
     *
     * @param toJid target JID
     * @return new Message stanza
     */
    public Message createRequestMessage(String toJid) {
        return createRequest(new Message(), toJid);
    }

    /**
     * Sends the given XMPP packet over the backing transport. This accepts a
     * callback which is guaranteed to be invoked at a later point, either through
     * a normal response, error response, or timeout.
     *
     * @param packet packet to be sent
     * @param callback callback to be invoked on response or timeout
     * @param timeout timeout, in seconds, for this callback
     */
    public void send(Packet packet, final PacketCallback callback, int timeout) {
        final String key = packet.getID() + "#" + packet.getTo() + "#" + packet.getFrom();

        final OutgoingCall call = new OutgoingCall(packet.getClass(), callback);
        if (callbacks.putIfAbsent(key, call) == null) {
            // Timeout runnable to be invoked on packet expiry.
            Runnable timeoutTask = new Runnable() {
                @Override
                public void run() {
                    if (callbacks.remove(key, call)) {
                        callback.error(
                                FederationErrors.newFederationError(FederationError.Code.REMOTE_SERVER_TIMEOUT));
                    } else {
                        // Likely race condition where success has actually occurred. Ignore.
                    }
                }
            };
            call.start(timeoutExecutor.schedule(timeoutTask, timeout, TimeUnit.SECONDS));
            transport.sendPacket(packet);
        } else {
            String msg = "Could not send packet, ID already in-flight: " + key;
            LOG.warning(msg);

            // Invoke the callback with an internal error.
            callback.error(FederationErrors.newFederationError(FederationError.Code.UNDEFINED_CONDITION, msg));
        }
    }

    /**
     * Cause an immediate timeout for the given packet, which is presumed to have
     * already been sent via {@link #send}.
     */
    @VisibleForTesting
    void causeImmediateTimeout(Packet packet) {
        String key = packet.getID() + "#" + packet.getTo() + "#" + packet.getFrom();
        OutgoingCall call = callbacks.remove(key);
        if (call != null) {
            call.callback.error(FederationErrors.newFederationError(FederationError.Code.REMOTE_SERVER_TIMEOUT,
                    "Forced immediate timeout"));
        }
    }

    /**
     * Invoke the callback for a packet already identified as a response. This may
     * either invoke the error or normal callback as necessary.
     */
    private void response(Packet packet) {
        String key = packet.getID() + "#" + packet.getFrom() + "#" + packet.getTo();
        OutgoingCall call = callbacks.remove(key);

        if (call == null) {
            LOG.warning("Received response packet without paired request: " + packet.getID());
        } else {
            // Cancel the outstanding timeout.
            call.timeout.cancel(false);

            // Look for error condition and invoke the relevant callback.
            Element element = packet.getElement().element("error");
            if (element != null) {
                LOG.fine("Invoking error callback for: " + packet.getID());
                call.callback.error(toFederationError(new PacketError(element)));
            } else {
                if (call.responseType.equals(packet.getClass())) {
                    LOG.fine("Invoking normal callback for: " + packet.getID());
                    call.callback.run(packet);
                } else {
                    String msg = "Received mismatched response packet type: expected " + call.responseType
                            + ", given " + packet.getClass();
                    LOG.warning(msg);
                    call.callback.error(
                            FederationErrors.newFederationError(FederationError.Code.UNDEFINED_CONDITION, msg));
                }
            }

            // Clear call's reference to callback, otherwise callback only
            // becomes eligible for GC once the timeout expires, because
            // timeoutExecutor holds on to the call object till then, even
            // though we cancelled the timeout.
            call.callback = null;
        }
    }

    /**
     * Process IQ request stanzas. This encompasses XMPP disco, submit and history
     * requests/responses, and get/post signer info requests/responses.
     */
    private void processIqGetSet(IQ iq) {
        Element body = iq.getChildElement();
        if (body == null) {
            sendErrorResponse(iq, FederationErrors.badRequest("Malformed request, no IQ child"));
            return;
        }

        final String namespace = body.getQName().getNamespace().getURI();
        final boolean isIQSet;
        if (iq.getType().equals(IQ.Type.get)) {
            isIQSet = false;
        } else if (iq.getType().equals(IQ.Type.set)) {
            isIQSet = true;
        } else {
            throw new IllegalArgumentException("Can only process an IQ get/set.");
        }
        PacketCallback responseCallback = new IncomingCallback(iq);

        if (namespace.equals(XmppNamespace.NAMESPACE_PUBSUB)) {
            final Element pubsub = iq.getChildElement();
            final Element element = pubsub.element(isIQSet ? "publish" : "items");

            if (element.attributeValue("node").equals("wavelet")) {
                if (isIQSet) {
                    host.processSubmitRequest(iq, responseCallback);
                } else {
                    host.processHistoryRequest(iq, responseCallback);
                }
            } else if (element.attributeValue("node").equals("signer")) {
                if (isIQSet) {
                    host.processPostSignerRequest(iq, responseCallback);
                } else {
                    host.processGetSignerRequest(iq, responseCallback);
                }
            } else {
                sendErrorResponse(iq, FederationError.Code.BAD_REQUEST, "Unhandled pubsub request");
            }
        } else if (!isIQSet) {
            if (namespace.equals(XmppNamespace.NAMESPACE_DISCO_INFO)) {
                disco.processDiscoInfoGet(iq, responseCallback);
            } else if (namespace.equals(XmppNamespace.NAMESPACE_DISCO_ITEMS)) {
                disco.processDiscoItemsGet(iq, responseCallback);
            } else {
                sendErrorResponse(iq, FederationError.Code.BAD_REQUEST, "Unhandled IQ get");
            }
        } else {
            sendErrorResponse(iq, FederationError.Code.BAD_REQUEST, "Unhandled IQ set");
        }
    }

    /**
     * Processes Message stanzas. This encompasses wavelet updates, update acks,
     * and ping messages.
     */
    private void processMessage(Message message) {
        if (message.getChildElement("event", XmppNamespace.NAMESPACE_PUBSUB_EVENT) != null) {
            remote.update(message, new IncomingCallback(message));
        } else if (message.getChildElement("ping", XmppNamespace.NAMESPACE_WAVE_SERVER) != null) {
            // Respond inline to the ping.
            LOG.info("Responding to ping from: " + message.getFrom());
            Message response = XmppUtil.createResponseMessage(message);
            response.addChildElement("received", XmppNamespace.NAMESPACE_XMPP_RECEIPTS);
            transport.sendPacket(response);
        } else {
            sendErrorResponse(message, FederationError.Code.BAD_REQUEST, "Unhandled message type");
        }
    }

    /**
     * Helper method to send generic error responses, backed onto
     * {@link #sendErrorResponse(Packet, FederationError)}.
     */
    void sendErrorResponse(Packet request, FederationError.Code code) {
        sendErrorResponse(request, FederationErrors.newFederationError(code));
    }

    /**
     * Helper method to send error responses, backed onto
     * {@link #sendErrorResponse(Packet, FederationError)}.
     */
    void sendErrorResponse(Packet request, FederationError.Code code, String text) {
        sendErrorResponse(request, FederationErrors.newFederationError(code, text));
    }

    /**
     * Send an error request to the passed incoming request.
     *
     * @param request packet request, target is derived from its to/from
     * @param error error to be contained in response
     */
    void sendErrorResponse(Packet request, FederationError error) {
        if (error.getErrorCode() == FederationError.Code.OK) {
            throw new IllegalArgumentException("Can't send an error of OK!");
        }
        sendErrorResponse(request, toPacketError(error));
    }

    /**
     * Send an error response to the passed incoming request. Throws
     * IllegalArgumentException if the original packet is also an error, or is of
     * the IQ result type.
     *
     * According to RFC 3920 (9.3.1), the error packet may contain the original
     * packet. However, this implementation does not include it.
     *
     * @param request packet request, to/from is inverted for response
     * @param error packet error describing error condition
     */
    void sendErrorResponse(Packet request, PacketError error) {
        if (request instanceof IQ) {
            IQ.Type type = ((IQ) request).getType();
            if (!(type.equals(IQ.Type.get) || type.equals(IQ.Type.set))) {
                throw new IllegalArgumentException("May only return an error to IQ get/set, not: " + type);
            }
        } else if (request instanceof Message) {
            Message message = (Message) request;
            if (message.getType().equals(Message.Type.error)) {
                throw new IllegalArgumentException("Can't return an error to another message error");
            }
        } else {
            throw new IllegalArgumentException(
                    "Unexpected Packet subclass, expected Message/IQ: " + request.getClass());
        }

        LOG.fine("Sending error condition in response to " + request.getID() + ": " + error.getCondition().name());

        // Note that this does not include the original packet; just the ID.
        final Packet response = XmppUtil.createResponsePacket(request);
        response.setError(error);

        transport.sendPacket(response);
    }

    /**
     * Convert a FederationError instance to a PacketError. This may return
     * <undefined-condition> if the incoming error can't be understood.
     *
     * @param error the incoming error
     * @return a generated PacketError instance
     * @throws IllegalArgumentException if the OK error code is given
     */
    private static PacketError toPacketError(FederationError error) {
        Preconditions.checkArgument(error.getErrorCode() != FederationError.Code.OK);

        String tag = error.getErrorCode().name().toLowerCase().replace('_', '-');
        PacketError.Condition condition;
        try {
            condition = PacketError.Condition.fromXMPP(tag);
        } catch (IllegalArgumentException e) {
            condition = PacketError.Condition.undefined_condition;
            LOG.warning("Did not understand error condition, defaulting to: " + condition.name());
        }
        PacketError result = new PacketError(condition);
        if (error.hasErrorMessage()) {
            // TODO(thorogood): Hide this behind a flag so we don't always broadcast error cases.
            result.setText(error.getErrorMessage(), "en");
        }
        return result;
    }

    /**
     * Convert a PacketError instance to an internal FederationError. This may
     * return an error code of UNDEFINED_CONDITION if the incoming error can't be
     * understood.
     *
     * @param error the incoming PacketError
     * @return the generated FederationError instance
     */
    private static FederationError toFederationError(PacketError error) {
        String tag = error.getCondition().name().toUpperCase().replace('-', '_');
        FederationError.Code code;
        try {
            code = FederationError.Code.valueOf(tag);
        } catch (IllegalArgumentException e) {
            code = FederationError.Code.UNDEFINED_CONDITION;
        }
        FederationError.Builder builder = FederationError.newBuilder().setErrorCode(code);
        if (error.getText() != null) {
            builder.setErrorMessage(error.getText());
        }
        return builder.build();
    }
}