/* Copyright (c) 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 com.google.wave.api;
import com.google.wave.api.Participants.Role;
import com.google.wave.api.impl.WaveletData;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* A class that models a single wavelet instance.
*
* A single wavelet is composed of metadata, for example, id,creator, creation
* time, last modified time, title. A wavelet is also composed of a set of
* participants, a set of tags, a list of blips, and a data document, which
* is a map of key-value pairs of data.
*/
public class Wavelet implements Serializable {
/** Delimiter constants for robot participant id. */
private static final char ROBOT_ID_PROXY_DELIMITER = '+';
private static final char ROBOT_ID_VERSION_DELIMITER = '#';
private static final char ROBOT_ID_DOMAIN_DELIMITER = '@';
/** The id of the wave that owns this wavelet. */
private transient WaveId waveId;
/** The id of this wavelet. */
private transient WaveletId waveletId;
/** The serialized form of waveId **/
private String serializedWaveId;
/** The serialized form of waveletId **/
private String serializedWaveletId;
/** The participant id that created this wavelet. */
private final String creator;
/** This wavelet's creation time, in milliseconds. */
private final long creationTime;
/** This wavelet's last modified time, in milliseconds. */
private final long lastModifiedTime;
/** The id of the root blip of this wavelet. */
private final String rootBlipId;
/** The root thread of this wavelet. */
private final BlipThread rootThread;
/** The participants of this wavelet. */
private final Participants participants;
/** The data documents of this wavelet. */
private final DataDocuments dataDocuments;
/** The tags that this wavelet has. */
private final Tags tags;
/** The title of this wavelet. */
private String title;
/** The blips that are contained in this wavelet. */
@NonJsonSerializable private final Map<String, Blip> blips;
/** The conversation threads that are contained in this wavelet. */
@NonJsonSerializable private final Map<String, BlipThread> threads;
/** The operation queue to queue operation to the robot proxy. */
@NonJsonSerializable private OperationQueue operationQueue;
/** The address of the current robot. */
@NonJsonSerializable private String robotAddress;
/**
* Constructor.
*
* @param waveId the id of the wave that owns this wavelet.
* @param waveletId the id of this wavelet.
* @param creator the creator of this wavelet.
* @param creationTime the creation time of this wavelet.
* @param lastModifiedTime the last modified time of this wavelet.
* @param title the title of this wavelet.
* @param rootBlipId the root blip id of this wavelet.
* @param rootThread the root thread of this wavelet.
* @param participantRoles the roles for those participants
* @param participants the participants of this wavelet.
* @param dataDocuments the data documents of this wavelet.
* @param tags the tags that this wavelet has.
* @param blips the blips that are contained in this wavelet.
* @param threads the conversation threads that are contained in this wavelet.
* @param operationQueue the operation queue to queue operation to the robot
* proxy.
*/
Wavelet(WaveId waveId, WaveletId waveletId, String creator, long creationTime,
long lastModifiedTime, String title, String rootBlipId, BlipThread rootThread,
Map<String, String> participantRoles, Set<String> participants,
Map<String, String> dataDocuments, Set<String> tags, Map<String, Blip> blips,
Map<String, BlipThread> threads, OperationQueue operationQueue) {
this.waveId = waveId;
this.waveletId = waveletId;
this.creator = creator;
this.creationTime = creationTime;
this.lastModifiedTime = lastModifiedTime;
this.title = title;
this.rootBlipId = rootBlipId;
this.rootThread = rootThread;
this.participants = new Participants(participants, participantRoles, this, operationQueue);
this.dataDocuments = new DataDocuments(dataDocuments, this, operationQueue);
this.tags = new Tags(tags, this, operationQueue);
this.blips = blips;
this.threads = threads;
this.operationQueue = operationQueue;
}
/**
* Constructor.
*
* @param waveId the id of the wave that owns this wavelet.
* @param waveletId the id of this wavelet.
* @param rootBlipId the root blip id of this wavelet.
* @param rootThread the root thread of this wavelet.
* @param participants the participants of this wavelet.
* @param participantRoles the roles for those participants.
* @param blips the blips that are contained in this wavelet.
* @param threads the conversation threads that are contained in this wavelet.
* @param operationQueue the operation queue to queue operation to the robot
* proxy.
*/
Wavelet(WaveId waveId, WaveletId waveletId, String rootBlipId, BlipThread rootThread,
Set<String> participants, Map<String, String> participantRoles, Map<String, Blip> blips,
Map<String, BlipThread> threads, OperationQueue operationQueue) {
this.waveId = waveId;
this.waveletId = waveletId;
this.rootBlipId = rootBlipId;
this.rootThread = rootThread;
this.creator = null;
this.creationTime = -1;
this.lastModifiedTime = -1;
this.title = null;
this.participants = new Participants(participants, participantRoles, this, operationQueue);
this.dataDocuments = new DataDocuments(new HashMap<String, String>(), this,
operationQueue);
this.tags = new Tags(Collections.<String>emptySet(), this, operationQueue);
this.blips = blips;
this.threads = threads;
this.operationQueue = operationQueue;
}
/**
* Shallow copy constructor.
*
* @param other the other {@link Wavelet} instance to copy.
* @param operationQueue the operation queue for this new wavelet instance.
*/
private Wavelet(Wavelet other, OperationQueue operationQueue) {
this.waveId = other.waveId;
this.waveletId = other.waveletId;
this.creator = other.creator;
this.creationTime = other.creationTime;
this.lastModifiedTime = other.lastModifiedTime;
this.title = other.title;
this.rootBlipId = other.rootBlipId;
this.rootThread = other.rootThread;
this.participants = other.participants;
this.dataDocuments = other.dataDocuments;
this.tags = other.tags;
this.blips = other.blips;
this.threads = other.threads;
this.robotAddress = other.robotAddress;
this.operationQueue = operationQueue;
}
/**
* Returns the wave id that owns this wavelet.
*
* @return the wave id.
*/
public WaveId getWaveId() {
return waveId;
}
/**
* Returns the id of this wavelet.
*
* @return the wavelet id.
*/
public WaveletId getWaveletId() {
return waveletId;
}
/**
* Returns the participant id of the creator of this wavelet.
*
* @return the creator of this wavelet.
*/
public String getCreator() {
return creator;
}
/**
* Returns the creation time of this wavelet.
*
* @return the wavelet's creation time.
*/
public long getCreationTime() {
return creationTime;
}
/**
* Returns the last modified time of this wavelet.
*
* @return the wavelet's last modified time.
*/
public long getLastModifiedTime() {
return lastModifiedTime;
}
/**
* Returns the data documents of this wavelet, which is a series of key-value
* pairs of data.
*
* @return an instance of {@link DataDocuments}, which represents the data
* documents of this wavelet.
*/
public DataDocuments getDataDocuments() {
return dataDocuments;
}
/**
* Returns a list of participants of this wavelet.
*
* @return an instance of {@link Participants}, which represents the
* participants of this wavelet.
*/
public Participants getParticipants() {
return participants;
}
/**
* Returns a list of tags of this wavelet.
*
* @return an instance of {@link Tags}, which represents the tags that have
* been associated with this wavelet.
*/
public Tags getTags() {
return tags;
}
/**
* Returns the title of this wavelet.
*
* @return the wavelet title.
*/
public String getTitle() {
return title;
}
/**
* Sets the wavelet title.
*
* @param title the new title to be set.
*/
public void setTitle(String title) {
if (title.contains("\n")) {
throw new IllegalArgumentException("Wavelet title should not contain a newline character. " +
"Specified: " + title);
}
operationQueue.setTitleOfWavelet(this, title);
this.title = title;
// Adjust the content of the root blip, if it is available in the context.
Blip rootBlip = getRootBlip();
if (rootBlip != null) {
String content = "\n";
int indexOfSecondNewline = rootBlip.getContent().indexOf('\n', 1);
if (indexOfSecondNewline != -1) {
content = rootBlip.getContent().substring(indexOfSecondNewline);
}
rootBlip.setContent("\n" + title + content);
}
}
/**
* Returns the address of the robot that receives events or performs events
* on this wavelet.
*
* @return the robot address.
*/
public String getRobotAddress() {
return robotAddress;
}
/**
* Sets the address of the robot that receives events or performs events on
* this wavelet.
*
* @param address the robot address.
* @throw IllegalStateException if this method has been called before.
*/
public void setRobotAddress(String address) {
if (this.robotAddress != null) {
throw new IllegalStateException("Robot address has been set previously to " +
this.robotAddress);
}
this.robotAddress = address;
}
/**
* Returns the id of the root blip of this wavelet.
*
* @return the id of the root blip.
*/
public String getRootBlipId() {
return rootBlipId;
}
/**
* Returns the root blip of this wavelet.
*
* @return an instance of {@link Blip} that represents the root blip of this
* wavelet.
*/
public Blip getRootBlip() {
return blips.get(rootBlipId);
}
/**
* @return an instance of {@link BlipThread} that represents the root thread of
* this wavelet.
*/
public BlipThread getRootThread() {
return rootThread;
}
/**
* Returns a blip with the given id.
*
* @return an instance of {@link Blip} that has the given blip id.
*/
public Blip getBlip(String blipId) {
return blips.get(blipId);
}
/**
* Returns all blips that are in this wavelet.
*
* @return a map of blips in this wavelet, that is keyed by blip id.
*/
public Map<String, Blip> getBlips() {
return blips;
}
/**
* Returns a thread with the given id.
*
* @param threadId the thread id.
* @return a thread that has the given thread id.
*/
public BlipThread getThread(String threadId) {
if (threadId == null || threadId.isEmpty()) {
return getRootThread();
}
return threads.get(threadId);
}
/**
* Returns all threads that are in this wavelet.
*
* @return a map of threads in this wavelet, that is keyed by thread id.
*/
public Map<String, BlipThread> getThreads() {
return threads;
}
/**
* Adds a thread to this wavelet.
*
* @param thread the thread to add.
*/
protected void addThread(BlipThread thread) {
threads.put(thread.getId(), thread);
}
/**
* Returns the operation queue that this wavelet uses to queue operation to
* the robot proxy.
*
* @return an instance of {@link OperationQueue} that represents this
* wavelet's operation queue.
*/
protected OperationQueue getOperationQueue() {
return operationQueue;
}
/**
* Returns the domain of this wavelet.
*
* @return the wavelet domain, which is encoded in the wave/wavelet id.
*/
public String getDomain() {
return waveId.getDomain();
}
/**
* Returns a view of this wavelet that will proxy for the specified id.
*
* A shallow copy of the current wavelet is returned with the
* {@code proxyingFor} field set. Any modifications made to this copy will be
* done using the {@code proxyForId}, i.e. the
* {@code robot+<proxyForId>@appspot.com} address will be used.
*
* If the wavelet was retrieved using the Active Robot API, that is
* by {@code fetchWavelet}, then the address of the robot must be added to the
* wavelet by calling {@code setRobotAddress} with the robot's address
* before calling {@code proxy_for}.
*
* @param proxyForId the id to proxy. Please note that this parameter should
* be properly encoded to ensure that the resulting participant id is
* valid (see {@link Util#checkIsValidProxyForId(String)} for more
* details).
* @return a shallow copy of this wavelet with the proxying information set.
*/
public Wavelet proxyFor(String proxyForId) {
Util.checkIsValidProxyForId(proxyForId);
addProxyingParticipant(proxyForId);
OperationQueue proxiedOperationQueue = operationQueue.proxyFor(proxyForId);
return new Wavelet(this, proxiedOperationQueue);
}
/**
* Submit this wavelet when the given {@code other} wavelet is submitted.
*
* Wavelets constructed outside of the event callback need to
* be either explicitly submitted using {@code AbstractRobot.submit(Wavelet)}
* or be associated with a different wavelet that will be submitted or is part
* of the event callback.
*
* @param other the other wavelet whose operation queue will be joined with.
*/
public void submitWith(Wavelet other) {
operationQueue.submitWith(other.operationQueue);
}
/**
* Replies to the conversation in this wavelet.
*
* @param initialContent the initial content of the reply.
* @return an instance of {@link Blip} that represents a transient version of
* the reply.
*
* @throws IllegalArgumentException if {@code initialContent} does not start
* with a newline character.
*/
public Blip reply(String initialContent) {
if (initialContent == null || !initialContent.startsWith("\n")) {
throw new IllegalArgumentException("Initial content should start with a newline character");
}
return operationQueue.appendBlipToWavelet(this, initialContent);
}
/**
* Removes a blip from this wavelet.
*
* @param blip the blip to be removed.
*/
public void delete(Blip blip) {
delete(blip.getBlipId());
}
/**
* Removes a blip from this wavelet.
*
* @param blipId the id of the blip to be removed.
*/
public void delete(String blipId) {
operationQueue.deleteBlip(this, blipId);
Blip removed = blips.remove(blipId);
if (removed != null) {
// Remove the blip from the parent blip.
Blip parentBlip = removed.getParentBlip();
if (parentBlip != null) {
parentBlip.deleteChildBlipId(blipId);
}
// Remove the blip from the containing thread.
BlipThread thread = removed.getThread();
if (thread != null) {
thread.removeBlip(removed);
}
// If the containing thread is now empty, remove it from the parent blip
// and from the wavelet.
if (thread != null && parentBlip != null && thread.isEmpty()) {
parentBlip.removeThread(thread);
threads.remove(thread.getId());
}
}
}
/**
* Ads a proxying participant to the wave.
*
* Proxying participants are of the form {@code robot+proxy@domain.com}. This
* convenience method constructs this id and then calls
* {@code wavelet.addParticipant()} operation.
*
* @param proxyForId the id to proxy.
*/
private void addProxyingParticipant(String proxyForId) {
if (robotAddress == null || robotAddress.isEmpty()) {
throw new IllegalStateException("Need a robot address to add a proxying for participant.");
}
// Parse the id and the domain.
int index = robotAddress.indexOf(ROBOT_ID_DOMAIN_DELIMITER);
String newId = robotAddress.substring(0, index);
String domain = robotAddress.substring(index + 1);
// Parse the version.
String version = null;
index = newId.indexOf(ROBOT_ID_VERSION_DELIMITER);
if (index != -1) {
version = newId.substring(index + 1);
newId = newId.substring(0, index);
}
// Remove the previous proxying id.
index = newId.indexOf(ROBOT_ID_PROXY_DELIMITER);
if (index != -1) {
newId = newId.substring(0, index);
}
// Assemble the new id.
newId += ROBOT_ID_PROXY_DELIMITER + proxyForId;
if (version != null) {
newId += ROBOT_ID_VERSION_DELIMITER + version;
}
newId += ROBOT_ID_DOMAIN_DELIMITER + domain;
participants.add(newId);
}
/**
* Serializes this {@link Wavelet} into a {@link WaveletData}.
*
* @return an instance of {@link WaveletData} that represents this wavelet.
*/
public WaveletData serialize() {
WaveletData waveletData = new WaveletData();
// Add primitive properties.
waveletData.setWaveId(waveId.serialise());
waveletData.setWaveletId(waveletId.serialise());
waveletData.setCreator(creator);
waveletData.setCreationTime(creationTime);
waveletData.setLastModifiedTime(lastModifiedTime);
waveletData.setRootBlipId(rootBlipId);
waveletData.setRootThread(rootThread);
waveletData.setTitle(title);
// Add tags.
List<String> tags = new ArrayList<String>();
for (String tag : this.tags) {
tags.add(tag);
}
waveletData.setTags(tags);
// Add participants.
List<String> participants = new ArrayList<String>();
for (String participant : this.participants) {
participants.add(participant);
Role role = getParticipants().getParticipantRole(participant);
waveletData.setParticipantRole(participant, role.name());
}
waveletData.setParticipants(participants);
// Add data documents.
Map<String, String> dataDocuments = new HashMap<String, String>();
for (Entry<String, String> entry : this.dataDocuments) {
dataDocuments.put(entry.getKey(), entry.getValue());
}
waveletData.setDataDocuments(dataDocuments);
return waveletData;
}
/**
* Deserializes the given {@link WaveletData} object into an instance of
* {@link Wavelet}.
*
* @param operationQueue the operation queue.
* @param blips the map of blips that are in this wavelet.
* @param waveletData the wavelet data to be deserialized.
* @return an instance of {@link Wavelet}.
*/
public static Wavelet deserialize(OperationQueue operationQueue, Map<String, Blip> blips,
Map<String, BlipThread> threads, WaveletData waveletData) {
WaveId waveId = WaveId.deserialise(waveletData.getWaveId());
WaveletId waveletId = WaveletId.deserialise(waveletData.getWaveletId());
String creator = waveletData.getCreator();
long creationTime = waveletData.getCreationTime();
long lastModifiedTime = waveletData.getLastModifiedTime();
String rootBlipId = waveletData.getRootBlipId();
BlipThread originalRootThread = waveletData.getRootThread();
List<String> rootThreadBlipIds = originalRootThread == null ?
new ArrayList<String>() :
new ArrayList<String>(originalRootThread.getBlipIds());
BlipThread rootThread = new BlipThread("", -1, rootThreadBlipIds, blips);
String title = waveletData.getTitle();
Set<String> participants = new LinkedHashSet<String>(waveletData.getParticipants());
Set<String> tags = new LinkedHashSet<String>(waveletData.getTags());
Map<String, String> dataDocuments = waveletData.getDataDocuments();
Map<String, String> roles = waveletData.getParticipantRoles();
return new Wavelet(waveId, waveletId, creator, creationTime, lastModifiedTime, title,
rootBlipId, rootThread, roles, participants, dataDocuments, tags, blips, threads,
operationQueue);
}
private void writeObject(ObjectOutputStream out) throws IOException {
serializedWaveId = waveId.serialise();
serializedWaveletId = waveletId.serialise();
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException {
try {
in.defaultReadObject();
} catch(ClassNotFoundException e) {
// Fatal.
throw new IOException("Incorrect serial versions" + e);
}
waveId = WaveId.deserialise(serializedWaveId);
waveletId = WaveletId.deserialise(serializedWaveletId);
}
}