/** * 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.util; import com.google.common.base.Preconditions; import org.waveprotocol.box.common.Receiver; import org.waveprotocol.box.server.waveserver.DeltaStore; import org.waveprotocol.box.server.waveserver.WaveletDeltaRecord; import org.waveprotocol.wave.model.document.util.EmptyDocument; import org.waveprotocol.wave.model.id.IdUtil; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.schema.SchemaCollection; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.BlipData; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.ReadableWaveletData; import org.waveprotocol.wave.model.wave.data.WaveViewData; import org.waveprotocol.wave.model.wave.data.WaveletData; import org.waveprotocol.wave.model.wave.data.impl.EmptyWaveletSnapshot; import org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument; import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.annotation.Nullable; /** * Utility methods for {@link WaveletData}. * * @author ljvderijk@google.com (Lennard de Rijk) */ public final class WaveletDataUtil { // TODO(ljvderijk): Schemas should be enforced, see issue 109. private static final ObservableWaveletData.Factory<?> WAVELET_FACTORY = WaveletDataImpl.Factory.create( ObservablePluggableMutableDocument.createFactory(SchemaCollection.empty())); private WaveletDataUtil() { } /** * Returns the {@link WaveletName} for the given wavelet. * * @param wavelet the wavelet to get the name for */ public static WaveletName waveletNameOf(ReadableWaveletData wavelet) { return WaveletName.of(wavelet.getWaveId(), wavelet.getWaveletId()); } /** * Apply a delta to the given wavelet. Rolls back the operation if it fails. * * @param delta delta to apply. * @param wavelet the wavelet to apply the operations to. * * @throws OperationException if the operations fail to apply (and are * successfully rolled back). * @throws IllegalStateException if the operations have failed and can not be * rolled back. */ public static void applyWaveletDelta(TransformedWaveletDelta delta, WaveletData wavelet) throws OperationException { Preconditions.checkState(wavelet != null, "wavelet may not be null"); Preconditions.checkState(delta.getAppliedAtVersion() == wavelet.getVersion(), "Delta's version %s doesn't apply to wavelet at %s", delta.getAppliedAtVersion(), wavelet.getVersion()); List<WaveletOperation> reverseOps = new ArrayList<WaveletOperation>(); WaveletOperation lastOp = null; int opsApplied = 0; try { for (WaveletOperation op : delta) { lastOp = op; List<? extends WaveletOperation> reverseOp = op.applyAndReturnReverse(wavelet); reverseOps.addAll(reverseOp); opsApplied++; } } catch (OperationException e) { // Deltas are atomic, so roll back all operations that were successful rollbackWaveletOperations(wavelet, reverseOps); throw new OperationException("Only applied " + opsApplied + " of " + delta.size() + " operations at version " + wavelet.getVersion() + ", rolling back, failed op was " + lastOp, e); } } /** * Like applyWaveletOperations, but throws an {@link IllegalStateException} * when ops fail to apply. Is used for rolling back operations. * * @param ops to apply for rollback */ private static void rollbackWaveletOperations(WaveletData wavelet, List<WaveletOperation> ops) { for (int i = ops.size() - 1; i >= 0; i--) { try { ops.get(i).apply(wavelet); } catch (OperationException e) { throw new IllegalStateException( "Failed to roll back operation with inverse " + ops.get(i), e); } } } /** * Creates an empty wavelet. * * @param waveletName the name of the wavelet. * @param author the author of the wavelet. * @param creationTimestamp the time at which the wavelet is created. */ public static ObservableWaveletData createEmptyWavelet(WaveletName waveletName, ParticipantId author, HashedVersion version, long creationTimestamp) { return copyWavelet(new EmptyWaveletSnapshot(waveletName.waveId, waveletName.waveletId, author, version, creationTimestamp)); } /** * Constructs the wavelet state after the application of the first delta. * * @param waveletName the name of the wavelet. * @param delta first delta to apply at version zero. */ public static ObservableWaveletData buildWaveletFromFirstDelta(WaveletName waveletName, TransformedWaveletDelta delta) throws OperationException { Preconditions.checkArgument(delta.getAppliedAtVersion() == 0, "first delta has non-zero version: %s", delta.getAppliedAtVersion()); ObservableWaveletData wavelet = createEmptyWavelet( waveletName, delta.getAuthor(), // creator HashedVersion.unsigned(0), // garbage hash, is overwritten by first delta below delta.getApplicationTimestamp()); // creation time applyWaveletDelta(delta, wavelet); return wavelet; } /** * Reads all deltas from the given iterator and constructs the end * wavelet state by successive application of all deltas beginning * from the empty wavelet. * * @param waveletName the name of the wavelet. * @param deltas non-empty, contiguous sequence of non-empty deltas beginning * from version zero. */ public static ObservableWaveletData buildWaveletFromDeltas(WaveletName waveletName, Iterator<TransformedWaveletDelta> deltas) throws OperationException { Preconditions.checkArgument(deltas.hasNext(), "empty deltas"); ObservableWaveletData wavelet = buildWaveletFromFirstDelta(waveletName, deltas.next()); while (deltas.hasNext()) { TransformedWaveletDelta delta = deltas.next(); applyWaveletDelta(delta, wavelet); } return wavelet; } public static ObservableWaveletData buildWaveletFromDeltaAccess( DeltaStore.DeltasAccess deltaAccess) throws OperationException, IOException { Preconditions.checkArgument(!deltaAccess.isEmpty(), "empty deltas"); WaveletDeltaRecord initDelta = deltaAccess.getDelta(0); final ObservableWaveletData wavelet = createEmptyWavelet( deltaAccess.getWaveletName(), initDelta.getAuthor(), // creator HashedVersion.unsigned(0), // garbage hash, is overwritten by first delta below initDelta.getApplicationTimestamp()); // creation time try { deltaAccess.getAllDeltas(new Receiver<WaveletDeltaRecord>() { @Override public boolean put(WaveletDeltaRecord deltaRecord) { try { applyWaveletDelta(deltaRecord.getTransformedDelta(), wavelet); } catch (OperationException e) { return false; } catch (IllegalStateException e) { return false; } return true; } }); } catch (Exception e) { throw new OperationException(e); } return wavelet; } /** * Copies a wavelet. * * @param wavelet the wavelet to copy. * @return A mutable copy. */ public static ObservableWaveletData copyWavelet(ReadableWaveletData wavelet) { return WAVELET_FACTORY.create(wavelet); } /** * Adds an empty blip to the given wavelet. * * @param wavelet the wavelet to add the blip to. * @param blipId the id of the blip to add. * @param author the author of this blip (will also be the only participant). * @param time the time to set in the blip as creation/lastmodified time. */ public static BlipData addEmptyBlip( WaveletData wavelet, String blipId, ParticipantId author, long time) { return wavelet.createDocument(blipId, author, Collections.<ParticipantId>singleton(author), EmptyDocument.EMPTY_DOCUMENT, time, time); } /** * @return true if the wave has conversational root wavelet. */ public static boolean hasConversationalRootWavelet(@Nullable WaveViewData wave) { if (wave == null) { return false; } for (ObservableWaveletData waveletData : wave.getWavelets()) { WaveletId waveletId = waveletData.getWaveletId(); if (IdUtil.isConversationRootWaveletId(waveletId)) { return true; } } return false; } /** * Checks if the user has access to the wavelet. * * @param snapshot the wavelet data. * @param user the user that wants to access the wavelet. * @param sharedDomainParticipantId the shared domain participant id. * @return true if the user has access to the wavelet. */ public static boolean checkAccessPermission(ReadableWaveletData snapshot, ParticipantId user, ParticipantId sharedDomainParticipantId) { return user != null && (snapshot == null || snapshot.getParticipants().contains(user) || (sharedDomainParticipantId != null && snapshot.getParticipants().contains(sharedDomainParticipantId))); } }