/** * 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.frontend; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import org.waveprotocol.box.common.DeltaSequence; import org.waveprotocol.box.common.comms.WaveClientRpc; import org.waveprotocol.box.server.waveserver.WaveBus; import org.waveprotocol.box.server.waveserver.WaveServerException; import org.waveprotocol.box.server.waveserver.WaveletProvider; import org.waveprotocol.box.server.waveserver.WaveletProvider.SubmitRequestListener; import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta; import org.waveprotocol.wave.model.id.IdFilter; 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.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.RemoveParticipant; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; 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.model.wave.data.ReadableWaveletData; import org.waveprotocol.wave.util.logging.Log; import java.util.Collection; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** * Implements {@link ClientFrontend}. * * When a wavelet is added and it's not at version 0, buffer updates until a * request for the wavelet's history has completed. */ public class ClientFrontendImpl implements ClientFrontend, WaveBus.Subscriber { private static final Log LOG = Log.get(ClientFrontendImpl.class); private final static AtomicInteger channel_counter = new AtomicInteger(0); private final WaveletProvider waveletProvider; private final WaveletInfo waveletInfo; /** * Creates a client frontend and subscribes it to the wave bus. * * @throws WaveServerException if the server fails during initialization. */ public static ClientFrontendImpl create(WaveletProvider waveletProvider, WaveBus wavebus, WaveletInfo waveletInfo) throws WaveServerException { ClientFrontendImpl impl = new ClientFrontendImpl(waveletProvider, waveletInfo); wavebus.subscribe(impl); return impl; } /** * Constructor. * * @param waveletProvider * @param waveDomain the server wave domain. It is assumed that the wave domain is valid. */ @VisibleForTesting ClientFrontendImpl( WaveletProvider waveletProvider, WaveletInfo waveletInfo) { this.waveletProvider = waveletProvider; this.waveletInfo = waveletInfo; } @Override public void openRequest(ParticipantId loggedInUser, WaveId waveId, IdFilter waveletIdFilter, Collection<WaveClientRpc.WaveletVersion> knownWavelets, OpenListener openListener) { LOG.info("received openRequest from " + loggedInUser + " for " + waveId + ", filter " + waveletIdFilter + ", known wavelets: " + knownWavelets); // TODO(josephg): Make it possible for this to succeed & return public // waves. if (loggedInUser == null) { openListener.onFailure("Not logged in"); return; } if (!knownWavelets.isEmpty()) { openListener.onFailure("Known wavelets not supported"); return; } try { waveletInfo.initialiseWave(waveId); } catch (WaveServerException e) { LOG.severe("Wave server failed lookup for " + waveId, e); openListener.onFailure("Wave server failed to look up wave"); return; } String channelId = generateChannelID(); UserManager userManager = waveletInfo.getUserManager(loggedInUser); WaveViewSubscription subscription = userManager.subscribe(waveId, waveletIdFilter, channelId, openListener); LOG.info("Subscribed " + loggedInUser + " to " + waveId + " channel " + channelId); Set<WaveletId> waveletIds; try { waveletIds = waveletInfo.visibleWaveletsFor(subscription, loggedInUser); } catch (WaveServerException e1) { waveletIds = Sets.newHashSet(); LOG.warning("Failed to retrieve visible wavelets for " + loggedInUser, e1); } for (WaveletId waveletId : waveletIds) { WaveletName waveletName = WaveletName.of(waveId, waveletId); // Ensure that implicit participants will also receive updates. // TODO (Yuri Z.) If authorizing participant was removed from the wave // (the shared domain participant), then all implicit participant that // were authorized should be unsubsrcibed. waveletInfo.notifyAddedImplcitParticipant(waveletName, loggedInUser); // The WaveletName by which the waveletProvider knows the relevant deltas // TODO(anorth): if the client provides known wavelets, calculate // where to start sending deltas from. CommittedWaveletSnapshot snapshotToSend; // Send a snapshot of the current state. // TODO(anorth): calculate resync point if the client already knows // a snapshot. try { snapshotToSend = waveletProvider.getSnapshot(waveletName); } catch (WaveServerException e) { LOG.warning("Failed to retrieve snapshot for wavelet " + waveletName, e); openListener.onFailure("Wave server failure retrieving wavelet"); return; } LOG.info("snapshot in response is: " + (snapshotToSend != null)); if (snapshotToSend == null) { // Send deltas. openListener.onUpdate(waveletName, snapshotToSend, DeltaSequence.empty(), null, null, channelId); } else { // Send the snapshot. openListener.onUpdate(waveletName, snapshotToSend, DeltaSequence.empty(), snapshotToSend.committedVersion, null, channelId); } } WaveletName dummyWaveletName = createDummyWaveletName(waveId); if (waveletIds.size() == 0) { // Send message with just the channel id. LOG.info("sending just a channel id for " + dummyWaveletName); openListener.onUpdate(dummyWaveletName, null, DeltaSequence.empty(), null, null, channelId); } LOG.info("sending marker for " + dummyWaveletName); openListener.onUpdate(dummyWaveletName, null, DeltaSequence.empty(), null, true, null); } private String generateChannelID() { return "ch" + channel_counter.addAndGet(1); } @Override public void submitRequest(ParticipantId loggedInUser, final WaveletName waveletName, final ProtocolWaveletDelta delta, final String channelId, final SubmitRequestListener listener) { final ParticipantId author = new ParticipantId(delta.getAuthor()); if (!author.equals(loggedInUser)) { listener.onFailure("Author field on delta must match logged in user"); return; } waveletInfo.getUserManager(author).submitRequest(channelId, waveletName); waveletProvider.submitRequest(waveletName, delta, new SubmitRequestListener() { @Override public void onSuccess(int operationsApplied, HashedVersion hashedVersionAfterApplication, long applicationTimestamp) { listener.onSuccess(operationsApplied, hashedVersionAfterApplication, applicationTimestamp); waveletInfo.getUserManager(author).submitResponse(channelId, waveletName, hashedVersionAfterApplication); } @Override public void onFailure(String error) { listener.onFailure(error); waveletInfo.getUserManager(author).submitResponse(channelId, waveletName, null); } }); } @Override public void waveletCommitted(WaveletName waveletName, HashedVersion version) { for (ParticipantId participant : waveletInfo.getWaveletParticipants(waveletName)) { waveletInfo.getUserManager(participant).onCommit(waveletName, version); } } /** * Sends new deltas to a particular user on a particular wavelet. * Updates the participants of the specified wavelet if the participant was added or removed. * * @param waveletName the waveletName which the deltas belong to. * @param participant on the wavelet. * @param newDeltas newly arrived deltas of relevance for participant. Must * not be empty. * @param add whether the participant is added by the first delta. * @param remove whether the participant is removed by the last delta. */ private void participantUpdate(WaveletName waveletName, ParticipantId participant, DeltaSequence newDeltas, boolean add, boolean remove) { if(LOG.isFineLoggable()) { LOG.fine("Notifying " + participant + " for " + waveletName); } if (add) { waveletInfo.notifyAddedExplicitWaveletParticipant(waveletName, participant); } waveletInfo.getUserManager(participant).onUpdate(waveletName, newDeltas); if (remove) { waveletInfo.notifyRemovedExplicitWaveletParticipant(waveletName, participant); } } /** * Tracks wavelet versions and ensures that the deltas are contiguous. Updates * wavelet subscribers with new new deltas. */ @Override public void waveletUpdate(ReadableWaveletData wavelet, DeltaSequence newDeltas) { if (newDeltas.isEmpty()) { return; } WaveletName waveletName = WaveletName.of(wavelet.getWaveId(), wavelet.getWaveletId()); if(waveletInfo.getCurrentWaveletVersion(waveletName).getVersion() == 0 && LOG.isWarningLoggable()) { LOG.warning("Wavelet does not appear to have been initialized by client. Continuing anyway."); } waveletInfo.syncWaveletVersion(waveletName, newDeltas); Set<ParticipantId> remainingparticipants = Sets.newHashSet(waveletInfo.getWaveletParticipants(waveletName)); // Participants added during the course of newDeltas. Set<ParticipantId> newParticipants = Sets.newHashSet(); for (int i = 0; i < newDeltas.size(); i++) { TransformedWaveletDelta delta = newDeltas.get(i); // Participants added or removed in this delta get the whole delta. for (WaveletOperation op : delta) { if (op instanceof AddParticipant) { ParticipantId p = ((AddParticipant) op).getParticipantId(); remainingparticipants.add(p); newParticipants.add(p); } if (op instanceof RemoveParticipant) { ParticipantId p = ((RemoveParticipant) op).getParticipantId(); remainingparticipants.remove(p); participantUpdate(waveletName, p, newDeltas.subList(0, i + 1), newParticipants.remove(p), true); } } } // Send out deltas to those who end up being participants at the end // (either because they already were, or because they were added). for (ParticipantId p : remainingparticipants) { boolean isNew = newParticipants.contains(p); participantUpdate(waveletName, p, newDeltas, isNew, false); } } @VisibleForTesting static WaveletName createDummyWaveletName(WaveId waveId) { final WaveletName dummyWaveletName = WaveletName.of(waveId, WaveletId.of(waveId.getDomain(), "dummy+root")); return dummyWaveletName; } }