/**
* 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.box.server.waveserver;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer;
import org.waveprotocol.box.server.persistence.PersistenceException;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolSignature;
import org.waveprotocol.wave.federation.Proto.ProtocolSignedDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.wave.RemoveParticipant;
import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletDelta;
import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.util.logging.Log;
import java.util.concurrent.Executor;
/**
* A local wavelet may be updated by submits. The local wavelet will perform
* operational transformation on the submitted delta and assign it the latest
* version of the wavelet.
*/
class LocalWaveletContainerImpl extends WaveletContainerImpl implements LocalWaveletContainer {
private static final Log LOG = Log.get(LocalWaveletContainerImpl.class);
private static final Function<RemoveParticipant, ParticipantId> PARTICIPANT_REMOVED_BY =
new Function<RemoveParticipant, ParticipantId>() {
@Override
public ParticipantId apply(RemoveParticipant op) {
return op.getParticipantId();
}
};
private static final Function<ParticipantId, String> DOMAIN_OF =
new Function<ParticipantId, String>() {
@Override
public String apply(ParticipantId participant) {
return participant.getDomain();
}
};
private static Iterable<ParticipantId> participantsRemovedBy(Iterable<WaveletOperation> ops) {
return Iterables.transform(Iterables.filter(ops, RemoveParticipant.class),
PARTICIPANT_REMOVED_BY);
}
private static ImmutableSet<String> domainsOf(Iterable<ParticipantId> participants) {
return ImmutableSet.copyOf(Iterables.transform(participants, DOMAIN_OF));
}
public LocalWaveletContainerImpl(WaveletName waveletName, WaveletNotificationSubscriber notifiee,
ListenableFuture<? extends WaveletState> waveletStateFuture, String waveDomain,
Executor storageContinuationExecutor) {
super(waveletName, notifiee, waveletStateFuture, waveDomain, storageContinuationExecutor);
}
@Override
public WaveletDeltaRecord submitRequest(WaveletName waveletName, ProtocolSignedDelta signedDelta)
throws OperationException, InvalidProtocolBufferException, InvalidHashException,
PersistenceException, WaveletStateException {
awaitLoad();
acquireWriteLock();
try {
checkStateOk();
HashedVersion before = getCurrentVersion();
WaveletDeltaRecord result = transformAndApplyLocalDelta(signedDelta);
HashedVersion after = getCurrentVersion();
// Only publish and persist the delta if it wasn't transformed away
// (right now it never is since the current OT algorithm doesn't transform ops away)
// and wasn't a duplicate of a previously applied delta.
if (!after.equals(before)) {
Preconditions.checkState(!result.isEmpty());
Preconditions.checkState(result.getAppliedAtVersion().equals(before));
ImmutableSet<String> domainsToNotify = domainsOf(Iterables.concat(
accessSnapshot().getParticipants(),
participantsRemovedBy(result.getTransformedDelta())));
notifyOfDeltas(ImmutableList.of(result), domainsToNotify);
// We always persist a local delta immediately after it's applied
// and after it's broadcast on the wave bus and to remote servers.
persist(result.getResultingVersion(), domainsToNotify);
}
return result;
} finally {
releaseWriteLock();
}
}
/**
* Apply a signed delta to a local wavelet. This assumes the caller has
* validated that the delta is at the correct version and can be applied to
* the wavelet. Must be called with writelock held.
*
* @param signedDelta the delta that is to be applied to wavelet.
* @return the transformed and applied delta.
* @throws OperationException if an error occurs during transformation or
* application
* @throws InvalidProtocolBufferException if the signed delta did not contain a valid delta
* @throws InvalidHashException if delta hash sanity checks fail
*/
private WaveletDeltaRecord transformAndApplyLocalDelta(ProtocolSignedDelta signedDelta)
throws OperationException, InvalidProtocolBufferException, InvalidHashException,
PersistenceException {
ProtocolWaveletDelta protocolDelta =
ByteStringMessage.parseProtocolWaveletDelta(signedDelta.getDelta()).getMessage();
Preconditions.checkArgument(protocolDelta.getOperationCount() > 0, "empty delta");
WaveletDelta transformed = maybeTransformSubmittedDelta(
CoreWaveletOperationSerializer.deserialize(protocolDelta));
// TODO(ljvderijk): a Clock needs to be injected here (Issue 104)
long applicationTimestamp = System.currentTimeMillis();
HashedVersion currentVersion = getCurrentVersion();
// This is always false right now because the current algorithm doesn't transform ops away.
if (transformed.size() == 0) {
Preconditions.checkState(currentVersion.getVersion() != 0,
"currentVersion can not be 0 if delta was transformed");
Preconditions.checkState(
transformed.getTargetVersion().getVersion() <= currentVersion.getVersion());
// The delta was transformed away. That's OK but we don't call either
// applyWaveletOperations(), because that will throw IllegalArgumentException, or
// commitAppliedDelta(), because empty deltas cannot be part of the delta history.
TransformedWaveletDelta emptyDelta = new TransformedWaveletDelta(transformed.getAuthor(),
transformed.getTargetVersion(), applicationTimestamp, transformed);
return new WaveletDeltaRecord(transformed.getTargetVersion(), null, emptyDelta);
}
if (!transformed.getTargetVersion().equals(currentVersion)) {
Preconditions.checkState(
transformed.getTargetVersion().getVersion() < currentVersion.getVersion());
// The delta was a duplicate of an existing server delta.
// We duplicate-eliminate it (don't apply it to the wavelet state and don't store it in
// the delta history) and return the server delta which it was a duplicate of
// (so delta submission becomes idempotent).
ByteStringMessage<ProtocolAppliedWaveletDelta> existingDeltaBytes =
lookupAppliedDelta(transformed.getTargetVersion());
TransformedWaveletDelta dupDelta = lookupTransformedDelta(transformed.getTargetVersion());
LOG.info("Duplicate delta " + dupDelta + " for wavelet " + getWaveletName());
// TODO(anorth): Replace these comparisons with methods on delta classes.
Preconditions.checkState(dupDelta.getAuthor().equals(transformed.getAuthor()),
"Duplicate delta detected but mismatched author, expected %s found %s",
transformed.getAuthor(), dupDelta.getAuthor());
Preconditions.checkState(Iterables.elementsEqual(dupDelta, transformed),
"Duplicate delta detected but mismatched ops, expected %s found %s",
transformed, dupDelta);
return new WaveletDeltaRecord(transformed.getTargetVersion(), existingDeltaBytes, dupDelta);
}
// Build the applied delta to commit
ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta =
AppliedDeltaUtil.buildAppliedDelta(signedDelta, transformed.getTargetVersion(),
transformed.size(), applicationTimestamp);
return applyDelta(appliedDelta, transformed);
}
@Override
public boolean isDeltaSigner(HashedVersion version, ByteString signerId) {
ByteStringMessage<ProtocolAppliedWaveletDelta> appliedDelta =
lookupAppliedDeltaByEndVersion(version);
if (appliedDelta == null) {
return false;
}
ProtocolSignedDelta signedDelta = appliedDelta.getMessage().getSignedOriginalDelta();
for (ProtocolSignature signature : signedDelta.getSignatureList()) {
if (signature.getSignerId().equals(signerId)) return true;
}
return false;
}
}