/**
* 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.concurrencycontrol.wave;
import org.waveprotocol.wave.common.logging.LoggerBundle;
import org.waveprotocol.wave.concurrencycontrol.channel.Accessibility;
import org.waveprotocol.wave.concurrencycontrol.channel.OperationChannel;
import org.waveprotocol.wave.concurrencycontrol.channel.OperationChannelMultiplexer;
import org.waveprotocol.wave.concurrencycontrol.common.CorruptionDetail;
import org.waveprotocol.wave.concurrencycontrol.common.ResponseCode;
import org.waveprotocol.wave.concurrencycontrol.wave.CcBasedWavelet.FailureHandler;
import org.waveprotocol.wave.model.id.IdFilter;
import org.waveprotocol.wave.model.id.IdGenerator;
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.wave.WaveletOperationContext;
import org.waveprotocol.wave.model.schema.SchemaProvider;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.IdentitySet;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.model.wave.ParticipationHelper;
import org.waveprotocol.wave.model.wave.WaveViewListener;
import org.waveprotocol.wave.model.wave.Wavelet;
import org.waveprotocol.wave.model.wave.data.DocumentFactory;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl;
import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletConfigurator;
import org.waveprotocol.wave.model.wave.opbased.WaveViewImpl.WaveletFactory;
import java.util.Map;
/**
* An implementation of a wave view based on wavelets that sit on top of a
* concurrency-control stack.
*
*/
public final class CcBasedWaveViewImpl implements CcBasedWaveView {
/**
* Handler for the view becoming disconnected.
*
* @see WaveDisconnectedHandler
*/
public interface DisconnectedHandler {
/**
* Called when this wave becomes permanently disconnected
*/
void onWaveDisconnected(CorruptionDetail detail);
}
/**
* A factory for flushable documents, that also remembers documents it has
* created.
*/
public interface CcDocumentFactory<D extends CcDocument> extends DocumentFactory<D> {
/**
* Gets the document that was created for a particular document identifier.
*
* @param waveletId wavelet in which the document exists
* @param blipId blip in which the document exists
* @return the document that was previously created for blip {@code blipId}
* in wavelet {@code waveletId}, or {@code null}.
*/
D get(WaveletId waveletId, String blipId);
}
/**
* Listens to a mux, placing wavelet models on top of operation channels
* that show up. Such wavelets are then passed to a listener.
*/
private static class MuxListener implements OperationChannelMultiplexer.Listener {
/**
* Interface through which this listener notifies something of new wavelets
* that show up on the mux.
*/
interface WaveletListener {
void onWaveletAdded(CcBasedWavelet w);
void onWaveletRemoved(CcBasedWavelet w);
}
/**
* Interface through which mux listeners are created.
*/
interface Factory {
MuxListener create(OpenListener openListener);
}
private final CcBasedWavelet.Factory waveletFactory;
private final DisconnectedHandler disconnectedHandler;
private final WaveletListener listener;
private final OpenListener openListener;
private final Map<WaveletId, CcBasedWavelet> wavelets = CollectionUtils.newHashMap();
private final TerminalWavelets terminalWavelets;
MuxListener(CcBasedWavelet.Factory waveletFactory, DisconnectedHandler disconnectedHandler,
WaveletListener listener, OpenListener openListener,
TerminalWavelets terminalWavelets) {
this.waveletFactory = waveletFactory;
this.disconnectedHandler = disconnectedHandler;
this.listener = listener;
this.openListener = openListener;
this.terminalWavelets = terminalWavelets;
}
@Override
public void onOperationChannelCreated(OperationChannel channel, ObservableWaveletData snapshot,
Accessibility accessibility) {
// New wavelet has come into existence. Build a model on top.
CcBasedWavelet wavelet = waveletFactory.create(channel, snapshot);
// Keep this wavelet around for future changes.
wavelets.put(wavelet.getId(), wavelet);
// Mark inaccessible if necessary.
if (accessibility == Accessibility.INACCESSIBLE) {
terminalWavelets.markTerminal(wavelet);
}
listener.onWaveletAdded(wavelet);
}
@Override
public void onOperationChannelRemoved(OperationChannel channel, WaveletId waveletId) {
CcBasedWavelet wavelet = wavelets.remove(waveletId);
terminalWavelets.clearTerminal(wavelet);
listener.onWaveletRemoved(wavelet);
}
@Override
public void onFailed(CorruptionDetail detail) {
if (disconnectedHandler != null) {
disconnectedHandler.onWaveDisconnected(detail);
}
}
@Override
public void onOpenFinished() {
openListener.onOpenFinished();
}
}
/**
* Tracks terminal wavelets across all wavelet channels.
* {@see CcBasedWaveView#isTerminal(Wavelet)}.
*/
private static class TerminalWavelets {
private final IdentitySet<Wavelet> terminalWavelets = CollectionUtils.createIdentitySet();
/**
* Marks a wavelet as terminal.
*/
public void markTerminal(Wavelet wavelet) {
terminalWavelets.add(wavelet);
}
/**
* Clears the terminated status of a wavelet.
*/
public void clearTerminal(Wavelet wavelet) {
terminalWavelets.remove(wavelet);
}
/**
* @return whether a given wavelet is terminal
*/
public boolean isTerminal(Wavelet wavelet) {
return terminalWavelets.contains(wavelet);
}
}
private final WaveViewImpl<CcBasedWavelet> view;
private final OperationChannelMultiplexer pipe;
private final IdFilter waveletFilter;
private final MuxListener.Factory muxListenerFactory;
private final TerminalWavelets terminalWavelets;
/**
* Creates a wave view. The new view needs to be {@link #open(OpenListener)
* opened} before use.
*
* @return a new wave view.
*/
public static CcBasedWaveView create(CcDocumentFactory<?> docFactory, SchemaProvider schemas,
WaveId waveId, ParticipantId userId,
final OperationChannelMultiplexer pipe, IdFilter waveletFilter,
IdGenerator idGenerator, final LoggerBundle logger,
WaveletOperationContext.Factory contextFactory, ParticipationHelper participationHelper,
final DisconnectedHandler disconnectedHandler, WaveletConfigurator configurator,
DuplexOpSinkFactory tapsFactory) {
// Local class to overcome the impedance mismatch between:
// *) the view implementation's policy of notifying listeners as soons as a
// new wavelet shows up;
// *) the mux's model of creating a new wavelet by creating a new operation
// channel (which makes it look like a new wavelet has just showed up); and
// *) the need to delay notifying listeners of new wavelets until after the
// configurator has had a chance to prepare it.
class LocalWaveletHolder {
private boolean isExpecting;
private CcBasedWavelet w;
void expect() {
isExpecting = true;
}
boolean isExpecting() {
return isExpecting;
}
void push(CcBasedWavelet w) {
this.w = w;
isExpecting = false;
}
CcBasedWavelet pop() {
CcBasedWavelet toReturn = w;
w = null;
return toReturn;
}
}
// Handle wavelet failure by closing the mux (ensuring no more ops are
// received) and disconnecting the wave (ensuring no more local
// modifications are made).
FailureHandler failureHandler = new FailureHandler() {
@Override
public void onWaveletFailed(OperationException failure) {
logger.error().log("CcBasedWavelet failed permanently", failure);
disconnectedHandler.onWaveDisconnected(
new CorruptionDetail(failure.isSchemaViolation()
? ResponseCode.SCHEMA_VIOLATION : ResponseCode.INVALID_OPERATION,
"CcBasedWavelet failed",
failure));
pipe.close();
}
};
final CcBasedWavelet.Factory waveletFactory =
new CcBasedWavelet.Factory(waveId, contextFactory, participationHelper, docFactory,
failureHandler, tapsFactory);
final LocalWaveletHolder holder = new LocalWaveletHolder();
WaveletFactory<CcBasedWavelet> factory = new WaveletFactory<CcBasedWavelet>() {
@Override
public CcBasedWavelet create(WaveId waveId, WaveletId waveletId, ParticipantId creator) {
// For wavelets created by this view instance, it is undesirable to
// notify listeners of the new wavelet here; it is better to notify
// listeners right before the exit of createWavelet(), AFTER the
// WaveletConfigurator has initialized the new wavelet (e.g., in case
// the creating account needs to be added as a participant). This is to
// prevent callbacks from making mutations to the wavelet before it has
// been configured.
holder.expect();
assert holder.isExpecting();
pipe.createOperationChannel(waveletId, creator);
// This should have created a new wavelet through the
// onOperationChannelCreated callback.
assert !holder.isExpecting();
return holder.pop();
}
};
final WaveViewImpl<CcBasedWavelet> view =
WaveViewImpl.create(factory, waveId, idGenerator, userId, configurator);
final MuxListener.WaveletListener waveletListener = new MuxListener.WaveletListener() {
@Override
public void onWaveletAdded(CcBasedWavelet wavelet) {
if (view.getWavelet(wavelet.getId()) != null) {
logger.error().log(
"Ignoring new channel for existing wavelet " + wavelet.getId() + " in wave "
+ view.getWaveId());
} else {
if (holder.isExpecting()) {
holder.push(wavelet);
} else {
view.addWavelet(wavelet);
}
}
}
@Override
public void onWaveletRemoved(CcBasedWavelet wavelet) {
view.removeWavelet(wavelet);
}
};
final TerminalWavelets accessibilityTracker = new TerminalWavelets();
MuxListener.Factory muxListenerFactory = new MuxListener.Factory() {
@Override
public MuxListener create(OpenListener openListener) {
return new MuxListener(waveletFactory, disconnectedHandler, waveletListener,
openListener, accessibilityTracker);
}
};
return new CcBasedWaveViewImpl(
pipe, waveletFilter, view, muxListenerFactory, accessibilityTracker);
}
/**
* Creates a wave view (dependency injection constructor).
*/
private CcBasedWaveViewImpl(OperationChannelMultiplexer pipe, IdFilter waveletFilter,
WaveViewImpl<CcBasedWavelet> view, MuxListener.Factory muxListenerFactory,
TerminalWavelets terminalWavelets) {
this.pipe = pipe;
this.waveletFilter = waveletFilter;
this.view = view;
this.muxListenerFactory = muxListenerFactory;
this.terminalWavelets = terminalWavelets;
}
//
// Channel lifecycle.
//
public void open(OpenListener openListener) {
pipe.open(muxListenerFactory.create(openListener), waveletFilter);
}
public void close() {
pipe.close();
}
//
// Accessibility.
//
@Override
public boolean isTerminal(Wavelet wavelet) {
return terminalWavelets.isTerminal(wavelet);
}
//
// WaveView implementation.
//
@Override
public CcBasedWavelet createRoot() {
return view.createRoot();
}
@Override
public CcBasedWavelet createWavelet() {
return view.createWavelet();
}
@Override
public CcBasedWavelet createUserData() {
return view.createUserData();
}
@Override
public CcBasedWavelet getWavelet(WaveletId waveletId) {
return view.getWavelet(waveletId);
}
@Override
public CcBasedWavelet getRoot() {
return view.getRoot();
}
@Override
public CcBasedWavelet getUserData() {
return view.getUserData();
}
@Override
public Iterable<? extends CcBasedWavelet> getWavelets() {
return view.getWavelets();
}
@Override
public WaveId getWaveId() {
return view.getWaveId();
}
//
// Observable extension.
//
@Override
public void addListener(WaveViewListener listener) {
view.addListener(listener);
}
@Override
public void removeListener(WaveViewListener listener) {
view.removeListener(listener);
}
}