/**
* 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.data.impl;
import org.waveprotocol.wave.model.document.operation.DocInitialization;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.util.Preconditions;
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.DocumentFactory;
import org.waveprotocol.wave.model.wave.data.DocumentOperationSink;
import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
import org.waveprotocol.wave.model.wave.data.ReadableBlipData;
import org.waveprotocol.wave.model.wave.data.ReadableWaveletData;
import org.waveprotocol.wave.model.wave.data.WaveletData;
import org.waveprotocol.wave.model.wave.data.WaveletDataListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Skeleton implementation of {@link WaveletData}.
*
* Implementations must implement the following. The underlying representation
* of the participant list and expose it via the protected
* {@link #getMutableParticipants()} method. The underlying mapping from doc ids
* to documents. The creation of documents from {@link DocumentOperationSink}s
* via the protected
* {@link #internalCreateDocument(String, ParticipantId, Collection, DocumentOperationSink, long, long)
* )} method.
*
* @param <B> The document data type.
*/
public abstract class AbstractWaveletData<B extends BlipData> implements ObservableWaveletData {
/** Id of the wave to which this wavelet belongs. */
private final WaveId waveId;
/** The identifier of this wavelet. */
private final WaveletId id;
/** The creator of the wavelet. It does not need to be a participant of the wavelet. */
private final ParticipantId creator;
/** Version number. */
private long version;
/** Wavelet hashed server version. */
private HashedVersion hashedVersion;
/** Creation time. */
private final long creationTime;
/** Last-modified time. */
private long lastModifiedTime;
/** The factory used to create the content of a new document. */
private final DocumentFactory<?> contentFactory;
/** The manager for broadcasting listener events. */
private final WaveletDataListenerManager listenerManager = new WaveletDataListenerManager();
/**
* Creates a new wavelet.
*
* @param id id of the wavelet
* @param creator creator of the wavelet
* @param creationTime timestamp of wavelet creation
* @param version initial version of the wavelet
* @param hashedVersion initial distinct server version of the wavelet
* @param lastModifiedTime initial last-modified time for the wavelet
* @param waveId id of the wave containing the wavelet
* @param contentFactory factory for creating new documents
*/
protected AbstractWaveletData(WaveletId id, ParticipantId creator, long creationTime,
long version, HashedVersion hashedVersion, long lastModifiedTime, WaveId waveId,
DocumentFactory<?> contentFactory) {
Preconditions.checkNotNull(id, "id cannot be null");
Preconditions.checkNotNull(waveId, "wave id cannot be null");
this.id = id;
this.creator = creator;
this.creationTime = creationTime;
this.version = version;
this.hashedVersion = hashedVersion;
this.lastModifiedTime = lastModifiedTime;
this.waveId = waveId;
this.contentFactory = contentFactory;
}
/**
* Creates a copy of the given wavelet data, except its documents and
* participants.
*
* @param data The ReadableWaveletData to copy the data from.
* @param contentFactory Factory for creating new documents.
*/
protected AbstractWaveletData(ReadableWaveletData data, DocumentFactory<?> contentFactory) {
this.id = data.getWaveletId();
this.creator = data.getCreator();
this.creationTime = data.getCreationTime();
this.version = data.getVersion();
this.hashedVersion = data.getHashedVersion();
this.lastModifiedTime = data.getLastModifiedTime();
this.waveId = data.getWaveId();
this.contentFactory = contentFactory;
}
/**
* @return mutable, ordered set of participants.
*/
protected abstract Set<ParticipantId> getMutableParticipants();
/**
* Creates a document in this wavelet (private factory method).
*
* @param docId new document id, which must be unique in this wavelet
* @param author new document's author
* @param contributors new document's contributors
* @param contentSink new document's content
* @param lastModifiedTime new document's last modified time
* @param lastModifiedVersion new document's last modified version
* @return a new document data
*/
protected abstract B internalCreateDocument(String docId, ParticipantId author,
Collection<ParticipantId> contributors, DocumentOperationSink contentSink,
long lastModifiedTime, long lastModifiedVersion);
@Override
public abstract B getDocument(String documentId);
@Override
public Set<ParticipantId> getParticipants() {
return Collections.unmodifiableSet(getMutableParticipants());
}
@Override
public boolean addParticipant(ParticipantId p) {
Set<ParticipantId> participants = getMutableParticipants();
if (participants.contains(p)) {
return false;
}
participants.add(p);
getListenerManager().onParticipantAdded(this, p);
return true;
}
@Override
public boolean addParticipant(ParticipantId p, int position) {
Set<ParticipantId> participants = getMutableParticipants();
if (participants.contains(p)) {
return false;
}
insert(participants, position, p);
getListenerManager().onParticipantAdded(this, p);
return true;
}
@Override
public boolean removeParticipant(ParticipantId p) {
Set<ParticipantId> participants = getMutableParticipants();
if (!participants.remove(p)) {
return false;
}
getListenerManager().onParticipantRemoved(this, p);
return true;
}
@Override
final public ParticipantId getCreator() {
return creator;
}
@Override
final public long getVersion() {
return version;
}
@Override
final public HashedVersion getHashedVersion() {
return hashedVersion;
}
@Override
final public long getCreationTime() {
return creationTime;
}
@Override
final public long getLastModifiedTime() {
return lastModifiedTime;
}
@Override
final public WaveId getWaveId() {
return waveId;
}
@Override
final public WaveletId getWaveletId() {
return id;
}
@Override
final public long setVersion(long newVersion) {
if (version != newVersion) {
long oldVersion = version;
version = newVersion;
listenerManager.onVersionChanged(this, oldVersion, newVersion);
return oldVersion;
} else {
return version;
}
}
@Override
final public HashedVersion setHashedVersion(HashedVersion newHashedVersion) {
if (!hashedVersion.equals(newHashedVersion)) {
HashedVersion oldHashedVersion = hashedVersion;
hashedVersion = newHashedVersion;
listenerManager.onHashedVersionChanged(this, oldHashedVersion, newHashedVersion);
return oldHashedVersion;
} else {
return hashedVersion;
}
}
@Override
final public long setLastModifiedTime(long newTime) {
if (newTime == lastModifiedTime) {
return newTime;
}
long oldTime = lastModifiedTime;
lastModifiedTime = newTime;
listenerManager.onLastModifiedTimeChanged(this, oldTime, newTime);
return oldTime;
}
@Override
public B createDocument(String docId, ParticipantId author,
Collection<ParticipantId> contributors, DocInitialization content,
long lastModifiedTime, long lastModifiedVersion) {
B doc = internalCreateDocument(docId, author, contributors,
contentFactory.create(id, docId, content), lastModifiedTime, lastModifiedVersion);
getListenerManager().onBlipDataAdded(this, doc);
return doc;
}
@Override
final public void addListener(WaveletDataListener listener) {
listenerManager.addListener(listener);
}
@Override
final public void removeListener(WaveletDataListener listener) {
listenerManager.removeListener(listener);
}
/**
* Gets the listener manager for this wavelet data. Package-private for use by
* BlipDataImpl.
*/
protected WaveletDataListenerManager getListenerManager() {
return listenerManager;
}
@Override
final public String toString() {
// Space before \n in case some logger swallows the newline.
StringBuilder b = new StringBuilder("WaveletDataImpl: " + getWaveId() + "/" + getWaveletId()
+ " \n[version:" + getVersion() + "]"
+ " \n[creator: " + getCreator() + "]"
+ " \n[participants: " + getParticipants() + "]"
+ " \n[creation time: " + getCreationTime() + "]"
+ " \n[lastModifiedTime:" + getLastModifiedTime() + "]");
b.append("\n[documents:");
for (String docId : getDocumentIds()) {
b.append(" \n [" + docId + ": " + getDocument(docId) + "]");
}
b.append(" \n]");
return b.toString();
}
/**
* Inserts an element at a specified position in a collection.
* It does so by iterating through the collection, removing the elements
* after the specified position, adding the given element at the end,
* and then adding the removed elements back.
*
* Note that this method does the right thing when collection is an
* insertion-ordered LinkedHashSet. If the collection is a {@link List},
* use {@link List#add(int, Object)} instead.
*
* @param collection A mutable, insertion-ordered collection.
* @param position The position within the collection to insert the element
* @param element The element to insert.
* @throws IndexOutOfBoundsException if {@code position < 0} or
* {@code position > collection.size()}.
*/
private static <T> void insert(Collection<T> collection, int position, T element) {
Preconditions.checkPositionIndex(position, collection.size());
if (position == collection.size()) {
collection.add(element);
return;
}
// We iterate through the collection to insert element at the required position.
Iterator<T> iterator = collection.iterator();
// First skip the first position participants.
for (int i = 0; i < position; i++) {
iterator.next();
}
// Then move the remainder aside.
List<T> remainder = new ArrayList<T>(collection.size() - position);
while (iterator.hasNext()) {
remainder.add(iterator.next());
iterator.remove();
}
Preconditions.checkState(collection.size() == position,
"size %s != position %s", collection.size(), position);
// Finally add p at the end and add the remainder back.
collection.add(element);
collection.addAll(remainder);
}
/**
* Copies all documents from {@code source}.
*
* @param source to get the documents from.
*/
protected void copyDocuments(ReadableWaveletData source) {
for (String docId : source.getDocumentIds()) {
ReadableBlipData docData = source.getDocument(docId);
this.createDocument(docData.getId(), docData.getAuthor(), docData.getContributors(),
docData.getContent().asOperation(), docData.getLastModifiedTime(),
docData.getLastModifiedVersion());
}
}
/**
* Copies participants from {@code source}.
*
* @param source to get the participants from.
*/
protected void copyParticipants(ReadableWaveletData source) {
this.getMutableParticipants().addAll(source.getParticipants());
}
}