/**
* 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.examples.fedone.waveserver;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.internal.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.waveprotocol.wave.crypto.SignatureException;
import org.waveprotocol.wave.crypto.UnknownSignerException;
import org.waveprotocol.wave.examples.fedone.common.HashedVersion;
import static org.waveprotocol.wave.examples.fedone.common.WaveletOperationSerializer.serialize;
import org.waveprotocol.wave.examples.fedone.util.Log;
import org.waveprotocol.wave.examples.fedone.waveserver.WaveletContainer.State;
import org.waveprotocol.wave.federation.FederationErrorProto.FederationError;
import org.waveprotocol.wave.federation.FederationErrors;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolHashedVersion;
import org.waveprotocol.wave.federation.Proto.ProtocolSignature;
import org.waveprotocol.wave.federation.Proto.ProtocolSignedDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolSignerInfo;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.model.document.operation.BufferedDocOp;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.waveserver.FederationHostBridge;
import org.waveprotocol.wave.waveserver.FederationRemoteBridge;
import org.waveprotocol.wave.waveserver.SubmitResultListener;
import org.waveprotocol.wave.waveserver.WaveletFederationListener;
import org.waveprotocol.wave.waveserver.WaveletFederationListener.Factory;
import org.waveprotocol.wave.waveserver.WaveletFederationProvider;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The main class that services the FederationHost, FederationRemote and ClientFrontend.
*
*
*/
@Singleton
public class WaveServerImpl implements WaveServer {
private static final Log LOG = Log.get(WaveServerImpl.class);
protected static final long HISTORY_REQUEST_LENGTH_LIMIT_BYTES = 1024 * 1024;
// -------------------------------------------------------------------------------------------
// PRIVATES INITIALIZED IN CONSTRUCTOR.
// -------------------------------------------------------------------------------------------
private final CertificateManager certificateManager;
private final Factory federationHostFactory;
private final RemoteWaveletContainer.Factory remoteWaveletContainerFactory;
private final LocalWaveletContainer.Factory localWaveletContainerFactory;
private final WaveletFederationProvider federationRemote;
private WaveletListener clientListener = null;
// -------------------------------------------------------------------------------------------
// MAPS FOR WAVES AND FEDERATION HOSTS.
// -------------------------------------------------------------------------------------------
private final Map<WaveId, Map<WaveletId, WaveletContainer>> waveMap =
new MapMaker().makeComputingMap(
new Function<WaveId, Map<WaveletId, WaveletContainer>>() {
@Override
public Map<WaveletId, WaveletContainer> apply(WaveId from) {
return Maps.newHashMap();
}
});
/** List of federation hosts for which we have listeners */
private final Map<String, WaveletFederationListener> federationHosts =
// Add a new entry to the map on demand.
new MapMaker().makeComputingMap(
new Function<String, WaveletFederationListener>() {
@Override
public WaveletFederationListener apply(String domain) {
return federationHostFactory.listenerForDomain(domain);
}
}
);
// -------------------------------------------------------------------------------------------
// IMPLEMENTATION OF THE WAVELET-FEDERATION-LISTENER.FACTORY INTERFACE USED BY THE FED HOST.
// -------------------------------------------------------------------------------------------
/**
* Listener for notifications coming from the Federation Remote. For now we accept updates
* for wavelets on any domain.
*/
public WaveletFederationListener listenerForDomain(final String domain) {
return new WaveletFederationListener() {
@Override
public void waveletDeltaUpdate(final WaveletName waveletName,
List<ByteString> rawAppliedDeltas, final WaveletUpdateCallback callback) {
Preconditions.checkArgument(!rawAppliedDeltas.isEmpty());
WaveletContainer wavelet = getWavelet(waveletName);
if (wavelet != null && wavelet.getState() == State.CORRUPTED) {
// TODO: throw away whole wavelet and start again
LOG.info("Received update for corrupt wavelet");
callback.onFailure(FederationErrors.badRequest("Corrupt wavelet"));
return;
}
// Turn raw serialised ByteStrings in to a more useful representation
List<ByteStringMessage<ProtocolAppliedWaveletDelta>> appliedDeltas = Lists.newArrayList();
for (ByteString delta : rawAppliedDeltas) {
try {
appliedDeltas.add(ByteStringMessage.from(
ProtocolAppliedWaveletDelta.getDefaultInstance(), delta));
} catch (InvalidProtocolBufferException e) {
LOG.info("Invalid applied delta protobuf for incoming " + waveletName, e);
safeMarkWaveletCorrupted(wavelet);
callback.onFailure(FederationErrors.badRequest("Invalid applied delta protocol buffer"));
return;
}
}
// Update wavelet container with the applied deltas
String error = null;
try {
final RemoteWaveletContainer remoteWavelet = getOrCreateRemoteWavelet(waveletName);
// Update this remote wavelet with the immediately incoming delta, providing a callback
// so that incoming historic deltas (as well as this delta) can be given to the
// clientListener at a later point
remoteWavelet.update(appliedDeltas, domain, federationRemote, certificateManager,
new RemoteWaveletDeltaCallback() {
@Override
public void onSuccess(DeltaSequence result) {
if (clientListener != null) {
Map<String, BufferedDocOp> documentState =
getWavelet(waveletName).getWaveletData().getDocuments();
clientListener.waveletUpdate(waveletName, result, result.getEndVersion(),
documentState);
} else {
LOG.warning("Got valid deltaSequence for " + waveletName
+ ", clientListener is null");
}
}
@Override
public void onFailure(String errorMessage) {
LOG.warning("Update failed: e" + errorMessage);
callback.onFailure(FederationErrors.badRequest(errorMessage));
}
});
// TODO: when we support federated groups, forward to federationHosts too.
} catch(WaveletStateException e) {
// HACK(jochen): TODO: fix the case of the missing history! ###
LOG.severe("DANGER WILL ROBINSON, WAVELET HISTORY IS INCOMPLETE!!!", e);
error = e.getMessage();
} catch (HostingException e) {
error = e.getMessage();
} catch (WaveServerException e) {
error = e.getMessage();
} catch (IllegalArgumentException e) {
error = "Bad wavelet name " + e.getMessage();
}
if (error == null) {
callback.onSuccess();
} else {
LOG.warning("incoming waveletUpdate: bad update, " + error);
callback.onFailure(FederationErrors.badRequest(error));
}
}
@Override
public void waveletCommitUpdate(WaveletName waveletName,
ProtocolHashedVersion committedVersion, WaveletUpdateCallback callback) {
Preconditions.checkNotNull(committedVersion);
if (clientListener != null) {
clientListener.waveletCommitted(waveletName, committedVersion);
} else {
LOG.warning("Client listener is null");
}
// Pretend we've committed it, there is no persistence
LOG.fine("Responding with success to wavelet commit on " + waveletName);
callback.onSuccess();
}
};
}
/**
* Set wavelet as corrupted, if not null.
*
* @param wavelet to set as corrupted, if not null
*/
private void safeMarkWaveletCorrupted(WaveletContainer wavelet) {
if (wavelet != null) {
wavelet.setState(State.CORRUPTED);
}
}
// -------------------------------------------------------------------------------------------
// METHODS IMPLEMENTING THE WAVELET-FEDERATION-PROVIDER INTERFACE USED BY THE FED REMOTE.
// -------------------------------------------------------------------------------------------
@Override
public void setListener(WaveletListener listener) {
clientListener = listener;
}
@Override
public void submitRequest(WaveletName waveletName, ProtocolSignedDelta signedDelta,
SubmitResultListener listener) {
// Disallow creation of wavelets by remote users.
try {
ByteStringMessage<ProtocolWaveletDelta> delta = ByteStringMessage.from(
ProtocolWaveletDelta.getDefaultInstance(), signedDelta.getDelta());
if (delta.getMessage().getHashedVersion().getVersion() == 0) {
LOG.warning("Remote user tried to submit delta at version 0 - disallowed. " + signedDelta);
listener.onFailure(FederationErrors.badRequest("Remote users may not create wavelets."));
return;
}
} catch (InvalidProtocolBufferException e) {
listener.onFailure(FederationErrors.badRequest("Signed delta contains invalid delta"));
return;
}
try {
checkWaveletHosting(true, waveletName);
certificateManager.verifyDelta(signedDelta);
submitDelta(waveletName, signedDelta, listener);
} catch (HostingException e) {
LOG.warning("Remote tried to submit to local wavelet", e);
listener.onFailure(FederationErrors.badRequest("Local wavelet update"));
} catch (SignatureException e) {
LOG.warning("Submit request: Delta failed verification. WaveletName: " + waveletName +
" delta: " + signedDelta, e);
listener.onFailure(FederationErrors.badRequest("Remote verification failed"));
} catch (UnknownSignerException e) {
LOG.warning("Submit request: unknown signer. WaveletName: " + waveletName +
"delta: " + signedDelta, e);
listener.onFailure(FederationErrors.badRequest("Unknown signer"));
}
}
@Override
public void requestHistory(WaveletName waveletName, String domain,
ProtocolHashedVersion startVersion, ProtocolHashedVersion endVersion,
long lengthLimit, HistoryResponseListener listener) {
WaveletContainer wc = getWavelet(waveletName);
if (wc == null) {
listener.onFailure(FederationErrors.badRequest("Wavelet " + waveletName
+ " does not exist."));
LOG.info("Request history: " + domain + " requested non-existent wavelet: " + waveletName);
} else {
// TODO: once we support federated groups, expand support to request
// remote wavelets too.
if (!isLocalWavelet(waveletName)) {
listener.onFailure(FederationErrors.badRequest("Wavelet " + waveletName
+ " not hosted here."));
LOG.info("Federation remote for domain: " + domain + " requested a remote wavelet: " +
waveletName);
} else {
try {
NavigableSet<ByteStringMessage<ProtocolAppliedWaveletDelta>> deltaHistory =
wc.requestHistory(startVersion, endVersion);
List<ByteString> deltaHistoryBytes = Lists.newArrayList();
for (ByteStringMessage<ProtocolAppliedWaveletDelta> d : deltaHistory) {
deltaHistoryBytes.add(d.getByteString());
}
// Now determine whether we received the entire requested wavelet history.
LOG.info("Found deltaHistory between " + startVersion + " - " + endVersion
+ ", returning to requester domain " + domain + " -- " + deltaHistory);
ProtocolHashedVersion hashedEndVersion =
ProtocolHashedVersion.newBuilder().setHistoryHash(endVersion.getHistoryHash())
.setVersion(endVersion.getVersion()).build();
listener.onSuccess(deltaHistoryBytes, hashedEndVersion, endVersion.getVersion());
// TODO: ### check length limit ??
// else {
// ProtocolAppliedWaveletDelta lastDelta = deltaHistory.last();
// long lastVersion = lastDelta.getHashedVersionAppliedAt().getVersion() +
// lastDelta.getOperationsApplied();
// listener.onSuccess(deltaHistory, lcv.getVersion(),
// lastVersion);
// }
} catch (WaveletStateException e) {
LOG.severe("Error retrieving wavelet history: " + waveletName + " " + startVersion +
" - " + endVersion);
listener.onFailure(FederationErrors.badRequest(
"Server error while retrieving wavelet history."));
}
}
}
}
@Override
public void getDeltaSignerInfo(ByteString signerId,
WaveletName waveletName, ProtocolHashedVersion deltaEndVersion,
DeltaSignerInfoResponseListener listener) {
WaveletContainer wavelet = getWavelet(waveletName);
if (wavelet == null) {
LOG.info("getDeltaSignerInfo for nonexistent wavelet " + waveletName);
listener.onFailure(FederationErrors.badRequest("Wavelet does not exist"));
} else if (!(wavelet instanceof LocalWaveletContainer)) {
LOG.info("getDeltaSignerInfo for remote wavelet " + waveletName);
listener.onFailure(FederationErrors.badRequest("Wavelet is not locally hosted"));
} else {
LocalWaveletContainer localWavelet = (LocalWaveletContainer) wavelet;
if (localWavelet.isDeltaSigner(deltaEndVersion, signerId)) {
ProtocolSignerInfo signerInfo = certificateManager.retrieveSignerInfo(signerId);
if (signerInfo == null) {
// Oh no! We are supposed to store it, and we already know they did sign this delta.
LOG.severe("No stored signer info for valid getDeltaSignerInfo on " + waveletName);
listener.onFailure(FederationErrors.badRequest("Unknown signer info"));
} else {
listener.onSuccess(signerInfo);
}
} else {
LOG.info("getDeltaSignerInfo was not authrorised for wavelet " + waveletName
+ ", end version " + deltaEndVersion);
listener.onFailure(FederationErrors.badRequest("Not authorised to get signer info"));
}
}
}
@Override
public void postSignerInfo(String destinationDomain, ProtocolSignerInfo signerInfo,
PostSignerInfoResponseListener listener) {
try {
certificateManager.storeSignerInfo(signerInfo);
} catch (SignatureException e) {
String error = "verification failure from domain " + signerInfo.getDomain();
LOG.warning("incoming postSignerInfo: " + error, e);
listener.onFailure(FederationErrors.badRequest(error));
return;
}
listener.onSuccess();
}
// -------------------------------------------------------------------------------------------
// METHODS IMPLEMENTING THE WAVELET-PROVIDER INTERFACE USED BY THE CLIENT FRONTEND.
// -------------------------------------------------------------------------------------------
@Override
public NavigableSet<ProtocolWaveletDelta> getHistory(WaveletName waveletName,
ProtocolHashedVersion startVersion, ProtocolHashedVersion endVersion) {
WaveletContainer wc = getWavelet(waveletName);
if (wc == null) {
LOG.info("Client request for history made for non-existent wavelet: " + waveletName);
return null;
} else {
NavigableSet<ProtocolWaveletDelta> deltaHistory = null;
try {
deltaHistory = wc.requestTransformedHistory(startVersion, endVersion);
} catch (AccessControlException e) {
LOG.warning("Client requested history with incorrect hashedVersion: " + waveletName + " " +
" startVersion: " + startVersion + " endVersion: " + endVersion);
} catch (WaveletStateException e) {
if (e.getState() == State.LOADING) {
LOG.severe("Client Frontend requested history for a remote wavelet that is not yet" +
"available - this should not happen as we have not sent an update for the wavelet.");
} else {
LOG.severe("Error retrieving wavelet history: " + waveletName + " " + startVersion +
" - " + endVersion);
}
}
return deltaHistory;
}
}
@Override
public <T> T getSnapshot(WaveletName waveletName, WaveletSnapshotBuilder<T> builder) {
WaveletContainer wc = getWavelet(waveletName);
if (wc == null) {
LOG.info("client requested snapshot for non-existent wavelet: " + waveletName);
return null;
} else {
return wc.getSnapshot(builder);
}
}
@Override
public void submitRequest(final WaveletName waveletName, ProtocolWaveletDelta delta,
final SubmitResultListener listener) {
// The submitted delta is now serialized, from now on it's the canonical delta (i.e. it should
// never be deserialized and regenerated anywhere (even in other waveservers).
certificateManager.signDelta(ByteStringMessage.fromMessage(delta),
new CertificateManager.SignatureResultListener() {
@Override
public void signatureResult(ProtocolSignedDelta signedDelta) {
submitDelta(waveletName, signedDelta, listener);
}
});
}
// -------------------------------------------------------------------------------------------
// CONSTRUCTOR AND PRIVATE UTILITY METHODS.
// -------------------------------------------------------------------------------------------
/**
* Constructor.
*
* @param certificateManager provider of certificates; it also determines which
* domains this wave server regards as local wavelets.
* @param federationHostFactory factory that returns federation host instance listening
* on a given domain.
* @param federationRemote federation remote interface
* @param localWaveletContainerFactory factory for local WaveletContainers
* @param remoteWaveletContainerFactory factory for remote WaveletContainers
*/
@Inject
public WaveServerImpl(CertificateManager certificateManager,
@FederationHostBridge WaveletFederationListener.Factory federationHostFactory,
@FederationRemoteBridge WaveletFederationProvider federationRemote,
LocalWaveletContainer.Factory localWaveletContainerFactory,
RemoteWaveletContainer.Factory remoteWaveletContainerFactory) {
this.certificateManager = certificateManager;
this.federationHostFactory = federationHostFactory;
this.federationRemote = federationRemote;
this.localWaveletContainerFactory = localWaveletContainerFactory;
this.remoteWaveletContainerFactory = remoteWaveletContainerFactory;
LOG.info("Wave Server configured to host local domains: "
+ certificateManager.getLocalDomains().toString());
// Preemptively add our own signer info to the certificate manager
try {
certificateManager.storeSignerInfo(
certificateManager.getLocalSigner().getSignerInfo().toProtoBuf());
} catch (SignatureException e) {
LOG.severe("Failed to add our own signer info to the certificate store", e);
}
}
private boolean isLocalWavelet(WaveletName waveletName) {
LOG.info("### WS is local? " + waveletName + " = " + certificateManager.getLocalDomains().
contains(waveletName.waveletId.getDomain()));
return certificateManager.getLocalDomains().contains(waveletName.waveletId.getDomain());
}
private boolean checkWaveletHosting(boolean isLocal, WaveletName waveletName)
throws HostingException {
boolean l = isLocalWavelet(waveletName);
if (l != isLocal) {
throw new HostingException("Wavelet (" + waveletName + ") which is " +
(l ? "local" : "remote") + " should have been " + (l ? "remote." : "local."));
}
return l == isLocal;
}
/**
* Returns a container for a remote wavelet. If it doesn't exist, it will be created.
* This method is only called in response to a Federation Remote doing an update
* or commit on this wavelet.
*
* @param waveletName name of wavelet
* @throws HostingException if the name refers to a local wavelet.
* @return an existing or new instance.
* @throws IllegalArgumentException on bad wavelet name
*/
private RemoteWaveletContainer getOrCreateRemoteWavelet(WaveletName waveletName) throws
HostingException {
checkWaveletHosting(false, waveletName);
synchronized (waveMap) {
Map<WaveletId, WaveletContainer> wave = waveMap.get(waveletName.waveId);
// This will blow up if we messed up and put a local wavelet in by mistake.
RemoteWaveletContainer wc = (RemoteWaveletContainer) wave.get(waveletName.waveletId);
if (wc == null) {
wc = remoteWaveletContainerFactory.create(waveletName);
wave.put(waveletName.waveletId, wc);
}
return wc;
}
}
/**
* Returns a container for a local wavelet. If it doesn't exist, it will be created.
* Local wavelets are retrieved or created by a federation host or client on behalf
* of a participant. The participant must have permission to access the wavelet if
* it already exists.
*
* @param waveletName name of wavelet
* @param participantId on who's behalf this wavelet is to be accessed.
* @throws HostingException if the name refers to a local wavelet.
* @throws AccessControlException if the participant may not access the wavelet.
* @return an existing or new instance.
* @throws WaveletStateException if the wavelet is in a bad state.
*/
private LocalWaveletContainer getOrCreateLocalWavelet(WaveletName waveletName,
ParticipantId participantId) throws
HostingException, AccessControlException, WaveletStateException {
checkWaveletHosting(true, waveletName);
synchronized (waveMap) {
Map<WaveletId, WaveletContainer> wave = waveMap.get(waveletName.waveId);
// This will blow up if we messed up and put a remote wavelet in by mistake.
LocalWaveletContainer wc = (LocalWaveletContainer) wave.get(waveletName.waveletId);
if (wc == null) {
wc = localWaveletContainerFactory.create(waveletName);
// TODO: HACK(Jochen): do we need a namespace policer here ??? ###
wave.put(waveletName.waveletId, wc);
} else {
if (!wc.checkAccessPermission(participantId)) {
throw new AccessControlException(participantId + " is not a participant: " + waveletName);
}
}
return wc;
}
}
/**
* Returns a generic wavelet container, when the caller doesn't need to validate whether
* its a local or remote wavelet.
*
* @param waveletName name of wavelet.
* @return an wavelet container or null if it doesn't exist.
*/
private WaveletContainer getWavelet(WaveletName waveletName) {
synchronized (waveMap) {
Map<WaveletId, WaveletContainer> wave = waveMap.get(waveletName.waveId);
return wave.get(waveletName.waveletId);
}
}
/**
* Callback interface for sending a list of certificates to a domain.
*/
private interface PostSignerInfoCallback {
public void done(int successCount);
}
/**
* Submit the delta to local or remote wavelets, return results via listener.
* Also broadcast updates to federationHosts and clientFrontend.
*
* TODO: for now a the WaveletFederationProvider will have
* made sure this is a local wavelet. Once we support
* federated groups, that test should be removed.
*/
private void submitDelta(final WaveletName waveletName, final ProtocolSignedDelta delta,
final SubmitResultListener resultListener) {
ByteStringMessage<ProtocolWaveletDelta> waveletDelta;
try {
waveletDelta = ByteStringMessage.from(
ProtocolWaveletDelta.getDefaultInstance(), delta.getDelta());
} catch (InvalidProtocolBufferException e) {
throw new IllegalArgumentException("Signed delta does not contain valid wavelet delta", e);
}
if (isLocalWavelet(waveletName)) {
DeltaApplicationResult submitResult;
final ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta;
LocalWaveletContainer wc = null;
try {
LOG.info("## WS: Got submit: " + waveletName + " delta: " + waveletDelta);
// TODO(arb): add v0 policer here.
wc = getOrCreateLocalWavelet(waveletName,
new ParticipantId(waveletDelta.getMessage().getAuthor()));
/*
* Synchronise on the wavelet container so that updates passed to clientListener and
* Federation listeners are ordered correctly. The application of deltas can happen in any
* order (due to OT).
*
* TODO(thorogood): This basically creates a second write lock (like the one held within
* wc.submitRequest) and extends it out around the broadcast code. Ideally what should
* happen is the update is pushed onto a queue which is handled in another thread.
*/
synchronized (wc) {
// Get the host domains before applying the delta in case
// the delta contains a removeParticipant operation.
Set<String> hostDomains = Sets.newHashSet(getParticipantDomains(wc));
submitResult = wc.submitRequest(waveletName, delta);
appliedDelta = submitResult.getAppliedDelta();
// return result to caller.
ProtocolHashedVersion resultingVersion =
serialize(HashedVersion.getHashedVersionAfter(appliedDelta));
LOG.info("## WS: Submit result: " + waveletName + " appliedDelta: " + appliedDelta);
resultListener.onSuccess(appliedDelta.getMessage().getOperationsApplied(),
resultingVersion, appliedDelta.getMessage().getApplicationTimestamp());
// Send the results to the client frontend
if (clientListener != null) {
Map<String, BufferedDocOp> documentState =
getWavelet(waveletName).getWaveletData().getDocuments();
LOG.info("Sending update to client listener: " + submitResult.getDelta());
clientListener.waveletUpdate(waveletName, ImmutableList.of(submitResult.getDelta()),
submitResult.getHashedVersionAfterApplication(), documentState);
}
// Capture any new domains from addParticipant operations.
hostDomains.addAll(getParticipantDomains(wc));
// Broadcast results to the remote servers, but make sure they all have our signatures
for (final String hostDomain : hostDomains) {
final WaveletFederationListener host = federationHosts.get(hostDomain);
host.waveletDeltaUpdate(waveletName, ImmutableList.of(appliedDelta.getByteString()),
new WaveletFederationListener.WaveletUpdateCallback() {
@Override
public void onSuccess() {
}
@Override
public void onFailure(FederationError error) {
LOG.warning("outgoing waveletDeltaUpdate failure: " + error);
}
});
// TODO: if persistence is added, don't send commit notice
host.waveletCommitUpdate(waveletName, resultingVersion,
new WaveletFederationListener.WaveletUpdateCallback() {
@Override
public void onSuccess() {
}
@Override
public void onFailure(FederationError error) {
LOG.warning("outgoing waveletCommitUpdate failure: " + error);
}
});
}
}
} catch (AccessControlException e) {
resultListener.onFailure(FederationErrors.badRequest(e.getMessage()));
return;
} catch (OperationException e) {
resultListener.onFailure(FederationErrors.badRequest(e.getMessage()));
return;
} catch (WaveletStateException e) {
resultListener.onFailure(FederationErrors.badRequest(e.getMessage()));
return;
} catch (IllegalArgumentException e) {
resultListener.onFailure(FederationErrors.badRequest(e.getMessage()));
return;
} catch (HostingException e) {
throw new IllegalStateException("Should not get HostingException after " +
"checking isLocalWavelet", e);
} catch (InvalidProtocolBufferException e) {
resultListener.onFailure(FederationErrors.badRequest(e.getMessage()));
return;
} catch (InvalidHashException e) {
resultListener.onFailure(FederationErrors.badRequest(e.getMessage()));
return;
} catch (EmptyDeltaException e) {
// This is okay, just succeed silently. Use an empty timestamp since nothing was applied.
resultListener.onSuccess(0, serialize(wc.getCurrentVersion()), 0);
}
} else {
// For remote wavelets post required signatures to the authorative server then send delta
postAllSignerInfo(delta.getSignatureList(), waveletName.waveletId.getDomain(),
new PostSignerInfoCallback() {
@Override public void done(int successCount) {
LOG.info("Remote: successfully sent " + successCount + " of "
+ delta.getSignatureCount() + " certs to " + waveletName.waveletId.getDomain());
federationRemote.submitRequest(waveletName, delta, resultListener);
}
});
}
}
/**
* Post a list of certificates to a domain and run a callback when all are finished. The
* callback will run whether or not all posts succeed.
*
* @param sigs list of signatures to post signer info for
* @param domain to post signature to
* @param callback to run when all signatures have been posted, successfully or unsuccessfully
*/
private void postAllSignerInfo(final List<ProtocolSignature> sigs, final String domain,
final PostSignerInfoCallback callback) {
// In the current implementation there should only be a single signer
if (sigs.size() != 1) {
LOG.warning(sigs.size() + " signatures to broadcast, expecting exactly 1");
}
final AtomicInteger resultCount = new AtomicInteger(sigs.size());
final AtomicInteger successCount = new AtomicInteger(0);
for (final ProtocolSignature sig : sigs) {
final ProtocolSignerInfo psi = certificateManager.retrieveSignerInfo(sig.getSignerId());
if (psi == null) {
LOG.warning("Couldn't find signer info for " + sig);
if (resultCount.decrementAndGet() == 0) {
LOG.info("Finished signature broadcast with " + successCount.get()
+ " successful, running callback");
callback.done(successCount.get());
}
} else {
federationRemote.postSignerInfo(domain, psi, new PostSignerInfoResponseListener() {
@Override
public void onFailure(FederationError error) {
LOG.warning("Failed to post " + sig + " to " + domain + ": " + error);
countDown();
}
@Override
public void onSuccess() {
LOG.info("Successfully broadcasted " + sig + " to " + domain);
successCount.incrementAndGet();
countDown();
}
private void countDown() {
if (resultCount.decrementAndGet() == 0) {
LOG.info("Finished signature broadcast with " + successCount.get()
+ " successful, running callback");
callback.done(successCount.get());
}
}
});
}
}
}
private Set<String> getParticipantDomains(LocalWaveletContainer lwc) {
Set<String> hosts = Sets.newHashSet();
Set<String> localDomains = certificateManager.getLocalDomains();
for (ParticipantId p : lwc.getParticipants()) {
String domain = p.getDomain();
if (localDomains.contains(domain)) {
// Ignore, don't re-federate to a local domain.
} else {
hosts.add(p.getDomain());
}
}
return ImmutableSet.copyOf(hosts);
}
}