/** * Copyright 2009 Google Inc. * * 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.waveprotocol.wave.federation.xmpp; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.inject.name.Named; import com.google.protobuf.ByteString; import org.dom4j.Element; import org.waveprotocol.wave.federation.FederationErrorProto.FederationError; import org.waveprotocol.wave.federation.FederationErrors; import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.waveserver.WaveletFederationListener; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import java.util.List; import java.util.Queue; import java.util.logging.Logger; /** * An instance of this class is created on demand for outgoing messages to * another wave Federation Remote. The wave server asks the WaveXmppComponent to * create these. */ class XmppFederationHostForDomain implements WaveletFederationListener { private static final Logger LOG = Logger.getLogger(XmppFederationHostForDomain.class.getCanonicalName()); // Timeout for outstanding listener updates sent over XMPP. private static final int XMPP_LISTENER_TIMEOUT = 30; private static enum DiscoStatus { PENDING, COMPLETED, FAILED } private final String remoteDomain; private final XmppManager manager; private final Queue<SuccessFailCallback<String, String>> queuedMessages = Lists.newLinkedList(); private final String jid; private String remoteJid; private DiscoStatus discoStatus = DiscoStatus.PENDING; public XmppFederationHostForDomain(final String domain, XmppManager manager, XmppDisco disco, @Named("xmpp_jid") String jid) { this.remoteDomain = domain; this.manager = manager; this.jid = jid; // start discovery. disco.discoverRemoteJid(remoteDomain, new SuccessFailCallback<String, String>() { @Override public void onSuccess(String result) { synchronized (queuedMessages) { discoCompleted(result); } } @Override public void onFailure(String errorMessage) { synchronized (queuedMessages) { discoFailed(errorMessage); } } }); } @Override public void waveletCommitUpdate(WaveletName waveletName, ProtocolHashedVersion committedVersion, WaveletUpdateCallback callback) { waveletUpdate(waveletName, null, committedVersion, callback); } @Override public void waveletDeltaUpdate(WaveletName waveletName, List<ByteString> appliedDeltas, WaveletUpdateCallback callback) { waveletUpdate(waveletName, appliedDeltas, null, callback); } /** * Called when XMPP discovery is complete. Sends queued messages. * * @param remoteJid the discovered remote JID for this domain */ private void discoCompleted(String remoteJid) { Preconditions.checkState(discoStatus == DiscoStatus.PENDING); Preconditions.checkNotNull(remoteJid); this.remoteJid = remoteJid; this.discoStatus = DiscoStatus.COMPLETED; LOG.info("Disco completed for " + remoteDomain + ", running " + queuedMessages.size() + " queued messages"); while (!queuedMessages.isEmpty()) { queuedMessages.poll().onSuccess(remoteJid); } } /** * Called when XMPP discovery fails. Queued messages are flushed. * * @param errorMessage */ private void discoFailed(String errorMessage) { Preconditions.checkState(discoStatus == DiscoStatus.PENDING); this.remoteJid = null; this.discoStatus = DiscoStatus.FAILED; LOG.warning("Disco failed for " + remoteDomain + ", failing " + queuedMessages.size() + " queued messages"); while (!queuedMessages.isEmpty()) { queuedMessages.poll().onFailure(errorMessage); } } /** * @return the current remote JID */ @VisibleForTesting String getRemoteJid() { return remoteJid; } /** * Sends a wavelet update message on behalf of the wave server. This method * may contain applied deltas, a commit notice, or both. * * @param waveletName the wavelet name * @param deltaList the deltas to include in the message, or null * @param committedVersion last committed version to include, or null * @param callback callback to invoke on delivery success/failure */ public void waveletUpdate(final WaveletName waveletName, final List<ByteString> deltaList, final ProtocolHashedVersion committedVersion, final WaveletUpdateCallback callback) { if ((deltaList == null || deltaList.isEmpty()) && committedVersion == null) { throw new IllegalArgumentException("Must send at least one delta, " + "or a last committed version notice, for the target wavelet: " + waveletName); } // If disco is not yet complete, register a runnable to invoke this method // at a later point in time. synchronized (queuedMessages) { if (discoStatus == DiscoStatus.PENDING) { LOG.info("Disco is pending for " + remoteDomain + ", queueing update (already " + queuedMessages.size() + " messages in queue)"); queuedMessages.offer(new SuccessFailCallback<String, String>() { @Override public void onSuccess(String remoteJid) { waveletUpdate(waveletName, deltaList, committedVersion, callback); } @Override public void onFailure(String errorMessage) { callback.onFailure(FederationErrors.newFederationError( FederationError.Code.RESOURCE_CONSTRAINT, errorMessage)); } }); return; } else if (discoStatus == DiscoStatus.FAILED) { // TODO(kalman): reactivate after a time period, or hook into FedEx, or something in order // to not blacklist this domain for the lifetime of this gateway. String error = "Disco failed for " + remoteDomain + ", ignoring update for " + waveletName; LOG.warning(error); callback.onFailure(FederationErrors.newFederationError( FederationError.Code.RESOURCE_CONSTRAINT, error)); return; } else { // Disco has completed, so we must have found a JID. Preconditions.checkState(remoteJid != null); } } Message message = new Message(); message.setType(Message.Type.normal); message.setFrom(jid); message.setTo(remoteJid); message.setID(XmppUtil.generateUniqueId()); message.addChildElement("request", XmppNamespace.NAMESPACE_XMPP_RECEIPTS); final String encodedWaveletName; try { encodedWaveletName = XmppUtil.waveletNameCodec.encode(waveletName); } catch (IllegalArgumentException e) { // TODO(thorogood): Error message. callback.onFailure(FederationError.newBuilder() .setErrorCode(FederationError.Code.BAD_REQUEST).build()); return; } Element itemElement = message.addChildElement("event", XmppNamespace.NAMESPACE_PUBSUB_EVENT) .addElement("items").addElement("item"); if (deltaList != null) { for (ByteString delta : deltaList) { Element waveletUpdate = itemElement.addElement("wavelet-update", XmppNamespace.NAMESPACE_WAVE_SERVER).addAttribute("wavelet-name", encodedWaveletName); waveletUpdate.addElement("applied-delta").addCDATA(Base64Util.encode(delta.toByteArray())); } } if (committedVersion != null) { Element waveletUpdate = itemElement.addElement("wavelet-update", XmppNamespace.NAMESPACE_WAVE_SERVER) .addAttribute("wavelet-name", encodedWaveletName); waveletUpdate.addElement("commit-notice").addAttribute("version", Long.toString(committedVersion.getVersion())).addAttribute("history-hash", Base64Util.encode(committedVersion.getHistoryHash())); } // Send the generated message through to the foreign XMPP server. manager.send(message, new PacketCallback() { @Override public void error(FederationError error) { callback.onFailure(error); } @Override public void run(Packet packet) { callback.onSuccess(); } }, XMPP_LISTENER_TIMEOUT); } }