/**
* Copyright 2008 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.model.operation.wave;
import org.waveprotocol.wave.model.operation.OperationException;
import org.waveprotocol.wave.model.operation.ReversibleOperation;
import org.waveprotocol.wave.model.operation.Visitable;
import org.waveprotocol.wave.model.wave.data.BlipData;
/**
* Operation class for a particular kind of wavelet operation that does something to a blip within a
* wavelet.
*
*/
public abstract class BlipOperation implements
ReversibleOperation<BlipOperation, BlipData>, Visitable<BlipOperationVisitor> {
enum UpdateContributorMethod {
ADD, /** Will add the author if not already present (i.e., is a maybeAdd) */
REMOVE, /** Will remove the author if already present (i.e., is a maybeRemove) */
NONE /** Will not alter the list. */
}
/** Context in which this operation occurs. */
protected final WaveletOperationContext context;
/**
* Constructs a blip operation.
*/
protected BlipOperation(WaveletOperationContext context) {
this(context, true);
}
/**
* Constructs a blip operation.
*/
protected BlipOperation(WaveletOperationContext context, boolean isWorthyOfAttribution) {
this.context = context;
this.isWorthyOfAttribution = isWorthyOfAttribution;
}
/**
* Gets the operation context.
*
* @return the operation context.
*/
public WaveletOperationContext getContext() {
return context;
}
/**
* Applies the logic in {@code #apply1(Blip)} to a blip, and updates its metadata.
*
* This should not be invoked directly by subclasses; use doApply instead.
*
* @param target blip to modify
* @throws OperationException if thrown by {@link #doApply(BlipData)}
*/
public final void apply(BlipData target) throws OperationException {
// Execute subtype logic first, because if the subtype logic throws an exception, we must
// leave this wrapper untouched as though the operation never happened. The subtype is
// responsible for making sure if they throw an exception they must leave themselves in a
// state as those the op never happened.
doApply(target);
// Update metadata second. This means subtypes should assume that the
// metadata of a blip will be at the old state if they look at it in their
// operation logic.
doUpdate(target);
}
/**
* Mutates a blip. Subclasses can arbitrarily override this to execute their logic.
*
* @param target blip to modify
* @throws OperationException if an error occurs in the application of this operation.
*/
protected abstract void doApply(BlipData target) throws OperationException;
/**
* Updates the metadata of a blip. An operation is free to choose whether or
* not if affects the metadata of a blip. If it does, it may find the
* contributor-handling method
* {@link #update(BlipData, UpdateContributorMethod)} useful.
*
* @param target blip to update
*/
protected abstract void doUpdate(BlipData target);
/**
* Whether this operation updates a blip's metadata: contributors,
* last-modified version, etc.
*
* @param blipId id of the blip to be updated
*/
protected abstract boolean updatesBlipMetadata(String blipId);
/**
* Creates the operation context for the reverse of an operation.
*
* @param target blip from which to extract state to be restored by the
* reverse operation
* @param versionDecrement Number of versions to decrement in reverse.
* @return context for a reverse of this operation.
*/
protected final WaveletOperationContext createReverseContext(BlipData target,
long versionDecrement) {
// For now, we don't care about version numbers with reverse context
return new WaveletOperationContext(context.getCreator(), target.getLastModifiedTime(),
-versionDecrement);
}
/**
* Creates the operation context for the reverse of an operation.
*
* @param target blip from which to extract state to be restored by the
* reverse operation
* @return context for a reverse of this operation.
*/
protected final WaveletOperationContext createReverseContext(BlipData target) {
return createReverseContext(target, context.getVersionIncrement());
}
//
// Helper methods for metadata updates, should subclasses choose to use them.
//
/**
* Whether this mutation is the insertion of an inline blip anchor.
* HACK(user): this is a temporary hack for preventing inline-replies
* from affecting the metadata of the parent blip.
*/
private final boolean isWorthyOfAttribution;
/**
* Checks whether this blip operation is worthy of attribution.
*
* @param blipId The blip id this operation applies to.
* @return whether this blip operation is worthy of attribution.
*/
public boolean isWorthyOfAttribution(String blipId) {
return isWorthyOfAttribution && WorthyChangeChecker.isBlipIdWorthy(blipId);
}
/**
* Updates a blip's metadata with this operation's context. Updating meta data is a sign
* that the content of the blip's document have changed.
*
* @param target blip to update
* @param method method for updating the contributor list
* @return the method to reverse any contributor change made by this update.
*/
protected final UpdateContributorMethod update(BlipData target, UpdateContributorMethod method) {
if (!updatesBlipMetadata(target.getId())) {
return UpdateContributorMethod.NONE;
}
UpdateContributorMethod reverse;
switch (method) {
case ADD:
if (!target.getContributors().contains(context.getCreator())) {
target.addContributor(context.getCreator());
reverse = UpdateContributorMethod.REMOVE;
} else {
reverse = UpdateContributorMethod.NONE;
}
break;
case REMOVE:
if (target.getContributors().contains(context.getCreator())) {
target.removeContributor(context.getCreator());
reverse = UpdateContributorMethod.ADD;
} else {
reverse = UpdateContributorMethod.NONE;
}
break;
case NONE:
default:
reverse = UpdateContributorMethod.NONE;
break;
}
target.setLastModifiedVersion(target.getWavelet().getVersion() + context.getVersionIncrement());
if (context.hasTimestamp()) {
target.setLastModifiedTime(context.getTimestamp());
}
return reverse;
}
}