/** * 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.wave.opbased; import org.waveprotocol.wave.model.document.Document; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.util.EmptyDocument; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.OperationRuntimeException; import org.waveprotocol.wave.model.operation.SilentOperationSink; import org.waveprotocol.wave.model.operation.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory; import org.waveprotocol.wave.model.operation.wave.NoOp; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.operation.wave.WaveletOperationContext; import org.waveprotocol.wave.model.util.CopyOnWriteSet; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.Blip; import org.waveprotocol.wave.model.wave.Constants; import org.waveprotocol.wave.model.wave.ObservableWavelet; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.ParticipationHelper; import org.waveprotocol.wave.model.wave.WaveletListener; import org.waveprotocol.wave.model.wave.data.BlipData; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.WaveletData; import org.waveprotocol.wave.model.wave.data.WaveletDataListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * The collaborative structure of the sinks in a set of wave, blip and document adapters is as * follows: * <pre> * document * :OpBasedDocument - - - - - - > :DocumentOperationSink * | * V outputSink * 0 fromDocument * | blip * :OpBasedBlip - - - - - - - > :BlipData * | * V outputSink * 0 fromBlip * | wave * :OpBasedWavelet - - - - - - - > :WaveletData * | * V * 0 outputSink * | * V (outgoing client ops) * * where -0 x represents a sink called x * ----> represents flow of operations * - - > represents operation application, either full (apply()) or partial (update()). * </pre> * * The operation sinks associated with the OpBasedXXX are only for applying and passing around * locally-sourced operations. The structure of each OpBasedXXX is as follows. Each OpBasedXXX * <em>is</em> sink for operations it produces directly. These operations are applied to the * adapted target, then sent to an output sink: * <code> * self.consume(op) = * op.apply(target); * outputSink.consume(op); * </code> * Each OpBasedXXX with sub-OpBasedXXX has a sink for operations produced by those sub-OpBasedXXX * (fromDocument and fromBlip). Operations received through such a sink are boxed, partially * applied to the target (just the update() method, since the sub-operation has already been applied * to the sub-component), then sent down to the output sink. * <code> * fromSub.consume(op) = * op' = box(op); * op'.update(target); * outputSink.consume(op'); * </code> * * Note that operations arriving from elsewhere are applied top-down through the WaveletData * implementation, and do not flow through these adapters. The collaboration between the adapters * appears as follows: * * <pre> * document * :OpBasedDocument - - - - - - - > :DocumentOperationSink * | A document * V outputSink | * 0 fromDocument | * | blip | * :OpBasedBlip - - - - - - - > :BlipData * | A blip * V outputSink | * 0 fromBlip | * | wave | * :OpBasedWavelet - - - - - - - > :WaveletData * | | * V | * 0 outputSink 0 localSink * | A * V (outgoing client ops) |(incoming server ops) * </pre> * */ public class OpBasedWavelet implements ObservableWavelet { /** * Factory method to create a read-only OpBased-wavelet adapter. * Sending any operations will cause an exception. * * @param wavelet simple data to adapt */ public static OpBasedWavelet createReadOnly(ObservableWaveletData wavelet) { return new OpBasedWavelet(wavelet.getWaveId(), wavelet, // This doesn't thrown an exception, the sinks will new BasicWaveletOperationContextFactory(null), ParticipationHelper.READONLY, SilentOperationSink.BLOCKED, SilentOperationSink.BLOCKED); } /** View context in which this wavelet exists. */ private final WaveId waveId; /** Primitive view of the underlying wave. */ private final ObservableWaveletData wavelet; /** Sink to which produced operations are sent for execution. */ private final SilentOperationSink<? super WaveletOperation> executor; /** Output sink to which produced operations are sent for notification. */ private final SilentOperationSink<? super WaveletOperation> output; /** List of adapt blip */ private final Map<String, OpBasedBlip> blips = new HashMap<String, OpBasedBlip>(); /** * Sink to which blip adapters should place locally-sourced operations that * have already executed. Blip adapters give a wavelet adapter boxed ops, * because they know their ids, allowing a wavelet adapter to have a single * sink for all blips rather than a sink per blip. */ private final SilentOperationSink<WaveletBlipOperation> fromBlip = new SilentOperationSink<WaveletBlipOperation>() { /** * Implements the strategy for consuming operations sent to this adapter from a blip * adapter, after the operation has already been applied locally. */ public void consume(WaveletBlipOperation op) { authorise(op); // Update the wave, then send out. op.update(wavelet); output.consume(op); } }; /** Helper through which operation contexts are created. */ private final WaveletOperationContext.Factory contextFactory; /** Helper to figure out wavelet participation. */ private final ParticipationHelper participationHelper; private final CopyOnWriteSet<WaveletListener> listeners = CopyOnWriteSet.create(); /** * A WaveletDataListener that forward the event to the listener of this object. */ private final WaveletDataListener eventForwarder = new WaveletDataListener() { @Override public void onParticipantAdded(WaveletData wavelet, ParticipantId participantId) { for (WaveletListener l : listeners) { l.onParticipantAdded(OpBasedWavelet.this, participantId); } } @Override public void onParticipantRemoved(WaveletData wavelet, ParticipantId participantId) { for (WaveletListener l : listeners) { l.onParticipantRemoved(OpBasedWavelet.this, participantId); } } @Override public void onBlipDataAdded(WaveletData waveletData, BlipData blip) { // adapt and fire a event OpBasedBlip oblip = adapt(blip); for (WaveletListener l : listeners) { l.onBlipAdded(OpBasedWavelet.this, oblip); } } @Override public void onBlipDataSubmitted(WaveletData waveletData, BlipData blip) { // adapt and fire a event OpBasedBlip oblip = adapt(blip); for (WaveletListener l : listeners) { l.onBlipSubmitted(OpBasedWavelet.this, oblip); } } @Override public void onLastModifiedTimeChanged(WaveletData waveletData, long oldTime, long newTime) { for (WaveletListener l : listeners) { l.onLastModifiedTimeChanged(OpBasedWavelet.this, oldTime, newTime); } } @Override public void onVersionChanged(WaveletData wavelet, long oldVersion, long newVersion) { for (WaveletListener l : listeners) { l.onVersionChanged(OpBasedWavelet.this, oldVersion, newVersion); } } @Override public void onHashedVersionChanged(WaveletData waveletData, HashedVersion oldHashedVersion, HashedVersion newHashedVersion) { for (WaveletListener l : listeners) { l.onHashedVersionChanged(OpBasedWavelet.this, oldHashedVersion, newHashedVersion); } } @Override public void onBlipDataTimestampModified( WaveletData waveletData, BlipData b, long oldTime, long newTime) { OpBasedBlip oblip = adapt(b); for (WaveletListener l : listeners) { l.onBlipTimestampModified(OpBasedWavelet.this, oblip, oldTime, newTime); } } @Override public void onBlipDataVersionModified( WaveletData waveletData, BlipData b, long oldVersion, long newVersion) { OpBasedBlip oblip = adapt(b); for (WaveletListener l : listeners) { l.onBlipVersionModified(OpBasedWavelet.this, oblip, oldVersion, newVersion); } } @Override public void onBlipDataContributorAdded( WaveletData waveletData, BlipData blip, ParticipantId contributorId) { OpBasedBlip oblip = adapt(blip); for (WaveletListener l : listeners) { l.onBlipContributorAdded(OpBasedWavelet.this, oblip, contributorId); } } @Override public void onBlipDataContributorRemoved( WaveletData waveletData, BlipData blip, ParticipantId contributorId) { OpBasedBlip oblip = adapt(blip); for (WaveletListener l : listeners) { l.onBlipContributorRemoved(OpBasedWavelet.this, oblip, contributorId); } } @Override @Deprecated public void onRemoteBlipDataContentModified(WaveletData waveletData, BlipData blip) { for (WaveletListener l : listeners) { l.onRemoteBlipContentModified(OpBasedWavelet.this, adapt(blip)); } } }; /** * Creates a OpBased-wavelet adapter. * * @param waveId that this wavelet is part of * @param wavelet simple data to adapt * @param contextFactory factory to produce contexts for new operations * @param participationHelper helper to figure out wavelet participation * @param executor sink that (only) executes operations locally * @param output sink to receive all produced operations after they * have executed */ public OpBasedWavelet(WaveId waveId, ObservableWaveletData wavelet, WaveletOperationContext.Factory contextFactory, ParticipationHelper participationHelper, SilentOperationSink<? super WaveletOperation> executor, SilentOperationSink<? super WaveletOperation> output) { this.waveId = waveId; this.wavelet = wavelet; this.contextFactory = contextFactory; this.participationHelper = participationHelper; this.executor = executor; this.output = output; wavelet.addListener(eventForwarder); } /** * Applies the op to the adapted wavelet and then outputs the op for remote * notification. Generally {@link #authoriseApplyAndSend(WaveletOperation)} * should be called instead. * * @param op to apply. */ private void applyAndSend(WaveletOperation op) { // Put the op on the execution sink. The sink guarantees that the op has // executed by the time consume() returns. executor.consume(op); // Send to output sink. output.consume(op); } /** * Grants authorisation for the given change then applies it locally to the * adapted wavelet and outputs the op for remote notification. * * @param op to authorise and apply. */ private void authoriseApplyAndSend(WaveletOperation op) { authorise(op); applyAndSend(op); } /** * Gains access for the author of an operation to perform changes if they are * not already able to. This should generally be called before the given * operation has been applied locally, and must be called before it is sent to * the output sink. It acceptable, however, for the given operation to have * been applied locally already if it does not make any changes to the * participant list. * * @return the add-participant operation injected as a side-effect to * to authorisation, or null if no operation was injected. */ private AddParticipant authorise(WaveletOperation op) { ParticipantId author = op.getContext().getCreator(); Set<ParticipantId> participantIds = getParticipantIds(); if (participantIds.contains(author)) { // Users on the participant list may submit ops directly. } else if (participantIds.isEmpty()) { // Model is unaware of how participants are allowed to join a wave when // there is no one to authorise them. Assume the op is authorised, leaving // it to another part of the system to reject it if necessary. } else { ParticipantId authoriser = null; authoriser = participationHelper.getAuthoriser(author, participantIds); if (authoriser != null) { AddParticipant authorisation = new AddParticipant(contextFactory.createContext(authoriser), author); applyAndSend(authorisation); return authorisation; } } return null; } /** * Handles an exception that occured from the local application of an operation produced by this * adapter. * * TODO(zdwang): Remove this, it's better to just throw exceptions at the source, the leave the * exception policy to the caller. */ void handleException(OperationException e) { // TODO(user): implement appropriate policy throw new OperationRuntimeException("OpBasedWavelet caught exception: " + e, e); } /** * Delegates to {@link WaveletOperationContext.Factory#createContext()}. * * This method is also used by collaborating {@link OpBasedBlip} adapters, hence is * package-private. * * @return a new operation context. */ WaveletOperationContext createContext() { return contextFactory.createContext(); } /** * Adapts a {@link Blip} to a {@link OpBasedBlip}, by creating a * {@link OpBasedBlip} that collaborates with this wavelet. * * This method is also used by collaborating {@link OpBasedBlip}s, hence is * package-private. * * @param blip primitive blip to wrap * @return a OpBased view of the given primitive blip. */ OpBasedBlip adapt(BlipData blip) { if (blip == null) { return null; } OpBasedBlip oblip = blips.get(blip.getId()); if (oblip == null) { oblip = new OpBasedBlip(blip, this, fromBlip); blips.put(blip.getId(), oblip); } return oblip; } @Override public Iterable<? extends Blip> getBlips() { final List<Blip> blips = new ArrayList<Blip>(); for (String documentId : wavelet.getDocumentIds()) { blips.add(adapt(wavelet.getDocument(documentId))); } return blips; } @Override public OpBasedBlip getBlip(String blipId) { BlipData blipData = wavelet.getDocument(blipId); if (blipData != null) { return adapt(blipData); } else { return null; } } @Override public OpBasedBlip createBlip(String id) { // Optimistically create the blip assuming this author submits the // first operation. WaveletOperationContext context = createContext(); BlipData newBlip = wavelet.createDocument(id, context.getCreator(), Collections.singleton(context.getCreator()), EmptyDocument.EMPTY_DOCUMENT, Constants.NO_TIMESTAMP, Constants.NO_VERSION); return adapt(newBlip); } @Override public ObservableDocument getDocument(String docId) { Blip blip = getBlip(docId); if (blip == null) { blip = createBlip(docId); } Document doc = blip.getContent(); if (!(doc instanceof ObservableDocument)) { Preconditions.illegalArgument("Document \"" + docId + "\" is not observable"); } return (ObservableDocument) doc; } @Override public Set<String> getDocumentIds() { return wavelet.getDocumentIds(); } // // Mutator to operation translations. // /** * Creates and consumes an {@link AddParticipant} operation. */ @Override public void addParticipant(ParticipantId participant) { if (!wavelet.getParticipants().contains(participant)) { // Authrorise and apply/send the op in separate stages to avoid sending // duplicate add-participant ops (authorise() may inject one). WaveletOperation addOp = new AddParticipant(createContext(), participant); WaveletOperation injectedOp = authorise(addOp); if (!addOp.equals(injectedOp)) { applyAndSend(addOp); } } } /** * Creates and consumes a {@link RemoveParticipant} operation. */ @Override public void removeParticipant(ParticipantId participant) { if (wavelet.getParticipants().contains(participant)) { authoriseApplyAndSend(new RemoveParticipant(createContext(), participant)); } } // // Vanilla accessors. // @Override public WaveId getWaveId() { return waveId; } @Override public WaveletId getId() { return wavelet.getWaveletId(); } @Override public long getCreationTime() { return wavelet.getCreationTime(); } @Override public ParticipantId getCreatorId() { return wavelet.getCreator(); } @Override public long getLastModifiedTime() { return wavelet.getLastModifiedTime(); } @Override public Set<ParticipantId> getParticipantIds() { return Collections.unmodifiableSet(wavelet.getParticipants()); } @Override public long getVersion() { return wavelet.getVersion(); } @Override public HashedVersion getHashedVersion() { return wavelet.getHashedVersion(); } @Override public void addListener(WaveletListener listener) { listeners.add(listener); } @Override public void removeListener(WaveletListener listener) { listeners.remove(listener); } @Override public int hashCode() { return 37 + wavelet.getWaveletId().hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (!(obj instanceof OpBasedWavelet)) { return false; } else { return wavelet.getWaveletId().equals(((OpBasedWavelet) obj).wavelet.getWaveletId()); } } @Override public String toString() { return "OpBasedWavelet { " + wavelet + " }"; } /** * Creates and consumes a {@link NoOp} (empty) operation. */ public void touch() { authoriseApplyAndSend(new NoOp(createContext())); } }