/* 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 org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A class that models a single blip instance. * * Blips are essentially the documents that make up a conversation, that contain * annotations, content and elements. */ public class Blip implements Serializable { /** The property key for blip id in an inline blip element. */ private static final String INLINE_BLIP_ELEMENT_ID_KEY = "id"; /** The {@link Pattern} object used to search markup content. */ private static final Pattern MARKUP_PATTERN = Pattern.compile("\\<.*?\\>"); /** The id of this blip. */ private final String blipId; /** The id of the parent blip, {@code null} for blips in the root thread. */ private final String parentBlipId; /** The containing thread. */ private final BlipThread thread; /** The ids of the children of this blip. */ private final List<String> childBlipIds; /** The inline reply threads, sorted by location/offset. */ private SortedMap<Integer, BlipThread> inlineReplyThreads; /** The reply threads. */ private final List<BlipThread> replyThreads; /** The participant ids of the contributors of this blip. */ private final List<String> contributors; /** The participant id of the creator of this blip. */ private final String creator; /** The last modified time of this blip. */ private final long lastModifiedTime; /** The version of this blip. */ private final long version; /** The list of annotations for the content. */ private final Annotations annotations; /** The wavelet that owns this blip. */ @NonJsonSerializable private final Wavelet wavelet; /** The operation queue to queue operation to the robot proxy. */ @NonJsonSerializable private final OperationQueue operationQueue; /** The blip content. */ private String content; /** The element contents of this blip. */ private SortedMap<Integer, Element> elements; /** * Constructor. * * @param blipId the id of this blip. * @param initialContent the initial content of the blip. * @param parentBlipId the id of the parent. * @param threadId the id of the containing thread. * @param wavelet the wavelet that owns this blip. */ Blip(String blipId, String initialContent, String parentBlipId, String threadId, Wavelet wavelet) { this(blipId, new ArrayList<String>(), initialContent, new ArrayList<String>(), null, -1, -1, parentBlipId, threadId, new ArrayList<Annotation>(), new TreeMap<Integer, Element>(), new ArrayList<String>(), wavelet); // Make sure that initial content is valid, and starts with newline. if (this.content == null || this.content.isEmpty()) { this.content = "\n"; } else if (!this.content.startsWith("\n")) { this.content = "\n" + this.content; } } /** * Constructor. * * @param blipId the id of this blip. * @param childBlipIds he ids of the children of this blip. * @param content the content of this blip. * @param contributors the participant ids of the contributors of this blip. * @param creator the participant id of the creator of this blip. * @param lastModifiedTime the last modified time of this blip. * @param version the version of this blip. * @param parentBlipId the id of the parent of this blip. * @param threadId the id of the parent thread of this blip. * @param annotations the list of annotations for this blip's content. * @param elements the element contents of this blip. * @param replyThreadIds the ids of this blip's reply threads. * @param wavelet the wavelet that owns this blip. */ Blip(String blipId, List<String> childBlipIds, String content, List<String> contributors, String creator, long lastModifiedTime, long version, String parentBlipId, String threadId, List<Annotation> annotations, Map<Integer, Element> elements, List<String> replyThreadIds, Wavelet wavelet) { this.blipId = blipId; this.content = content; this.childBlipIds = new ArrayList<String>(childBlipIds); this.contributors = new ArrayList<String>(contributors); this.creator = creator; this.lastModifiedTime = lastModifiedTime; this.version = version; this.parentBlipId = parentBlipId; this.thread = wavelet.getThread(threadId); this.annotations = new Annotations(); for (Annotation annotation : annotations) { this.annotations.add(annotation.getName(), annotation.getValue(), annotation.getRange().getStart(), annotation.getRange().getEnd()); } this.elements = new TreeMap<Integer, Element>(elements); this.wavelet = wavelet; this.operationQueue = wavelet.getOperationQueue(); // Populate reply threads. this.inlineReplyThreads = new TreeMap<Integer, BlipThread>(); this.replyThreads = new ArrayList<BlipThread>(); for (String replyThreadId : replyThreadIds) { BlipThread thread = wavelet.getThread(replyThreadId); if (thread.getLocation() != -1) { inlineReplyThreads.put(thread.getLocation(), thread); } else { replyThreads.add(thread); } } } /** * Shallow copy constructor. * * @param other the blip to copy. * @param operationQueue the operation queue for this new blip instance. */ private Blip(Blip other, OperationQueue operationQueue) { this.blipId = other.blipId; this.childBlipIds = other.childBlipIds; this.inlineReplyThreads = other.inlineReplyThreads; this.replyThreads = other.replyThreads; this.content = other.content; this.contributors = other.contributors; this.creator = other.creator; this.lastModifiedTime = other.lastModifiedTime; this.version = other.version; this.parentBlipId = other.parentBlipId; this.thread = other.thread; this.annotations = other.annotations; this.elements = other.elements; this.wavelet = other.wavelet; this.operationQueue = operationQueue; } /** * Returns the id of this blip. * * @return the blip id. */ public String getBlipId() { return blipId; } /** * Returns the id of the wave that owns this blip. * * @return the wave id. */ public WaveId getWaveId() { return wavelet.getWaveId(); } /** * Returns the id of the wavelet that owns this blip. * * @return the wavelet id. */ public WaveletId getWaveletId() { return wavelet.getWaveletId(); } /** * Returns the list of ids of this blip children. * * @return the children's ids. */ public List<String> getChildBlipIds() { return childBlipIds; } /** * Returns the list of child blips. * * @return the children of this blip. */ public List<Blip> getChildBlips() { List<Blip> result = new ArrayList<Blip>(childBlipIds.size()); for (String childId : childBlipIds) { Blip childBlip = wavelet.getBlips().get(childId); if (childBlip != null) { result.add(childBlip); } } return result; } /** * @return the inline reply threads of this blip, sorted by the offset. */ public Collection<BlipThread> getInlineReplyThreads() { return inlineReplyThreads.values(); } /** * @return the reply threads of this blip. */ public Collection<BlipThread> getReplyThreads() { return replyThreads; } /** * Returns the participant ids of the contributors of this blip. * * @return the blip's contributors. */ public List<String> getContributors() { return contributors; } /** * Returns the participant id of the creator of this blip. * * @return the blip's creator. */ public String getCreator() { return creator; } /** * Returns the last modified time of this blip. * * @return the blip's last modified time. */ public long getLastModifiedTime() { return lastModifiedTime; } /** * Returns the version of this blip. * * @return the blip's version. */ public long getVersion() { return version; } /** * Returns the id of this blip's parent, or {@code null} if this blip is in * the root thread. * * @return the blip's parent's id. */ public String getParentBlipId() { return parentBlipId; } /** * Returns the parent blip. * * @return the parent of this blip. */ public Blip getParentBlip() { if (parentBlipId == null) { return null; } return wavelet.getBlips().get(parentBlipId); } /** * @return the containing thread. */ public BlipThread getThread() { return thread; } /** * Checks whether this is a root blip or not. * * @return {@code true} if this is a root blip, denoted by {@code null} parent * id. */ public boolean isRoot() { return blipId.equals(wavelet.getRootBlipId()); } /** * Returns the annotations for this blip's content. * * @return the blip's annotations. */ public Annotations getAnnotations() { return annotations; } /** * Returns the elements content of this blip. * * @return the blip's elements. */ public SortedMap<Integer, Element> getElements() { return elements; } /** * Returns the text content of this blip. * * @return blip's content. */ public String getContent() { return content; } /** * Sets the content of this blip. * * @param content the blip's content. */ void setContent(String content) { if (!content.startsWith("\n")) { content = "\n" + content; } this.content = content; } /** * Returns the length/size of the blip, denoted by the length of this blip's * text content. * * @return the size of the blip. */ public int length() { return content.length(); } /** * Returns the wavelet that owns this Blip. * * @return the wavelet. */ public Wavelet getWavelet() { return wavelet; } /** * Returns the operation queue for sending outgoing operations to the robot * proxy. * * @return the operation queue. */ protected OperationQueue getOperationQueue() { return operationQueue; } /** * Returns a reference to the entire content of the blip. * * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs all() { return BlipContentRefs.all(this); } /** * Returns all references to this blip's content that match {@code target}. * * @param target the text to search for. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs all(String target) { return BlipContentRefs.all(this, target, -1); } /** * Returns all references to this blip's content that match {@code target}. * This blip references object will have at most {@code maxResult} hits. * * @param target the text to search for. * @param maxResult the maximum number of hits. Specify -1 for no limit. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs all(String target, int maxResult) { return BlipContentRefs.all(this, target, maxResult); } /** * Returns all references to this blip's content that match {@code target} and * {@code restrictions}. * * @param target the element type to search for. * @param restrictions the element properties that need to be matched. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs all(ElementType target, Restriction... restrictions) { return BlipContentRefs.all(this, target, -1, restrictions); } /** * Returns all references to this blip's content that match {@code target} and * {@code restrictions}. This blip references object will have at most * {@code maxResult} hits. * * @param target the element type to search for. * @param maxResult the maximum number of hits. Specify -1 for no limit. * @param restrictions the element properties that need to be matched. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs all(ElementType target, int maxResult, Restriction... restrictions) { return BlipContentRefs.all(this, target, maxResult, restrictions); } /** * Returns the first reference to this blip's content that matches * {@code target}. * * @param target the text to search for. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs first(String target) { return all(target, 1); } /** * Returns the first reference to this blip's content that matches * {@code target} and {@code restrictions}. * * @param target the type of element to search for. * @param restrictions the list of restrictions to filter the search. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs first(ElementType target, Restriction... restrictions) { return all(target, 1, restrictions); } /** * Returns the reference to this blip's content at the specified index. * * @param index the index to reference. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs at(int index) { return BlipContentRefs.range(this, index, index + 1); } /** * Returns the reference to this blip's content at the specified range. * * @param start the start index of the range to reference. * @param end the end index of the range to reference. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs range(int start, int end) { return BlipContentRefs.range(this, start, end); } /** * Appends the given argument (element, text, or markup) to the blip. * * @param argument the element, text, or markup to be appended. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs append(BlipContent argument) { return BlipContentRefs.all(this).insertAfter(argument); } /** * Appends the given string to the blip. * * @param argument the string to be appended. * @return an instance of {@link BlipContentRefs}. */ public BlipContentRefs append(String argument) { return BlipContentRefs.all(this).insertAfter(argument); } /** * Creates a reply to this blip. * * @return an instance of {@link Blip} that represents a reply to the blip. */ public Blip reply() { return operationQueue.createChildOfBlip(this); } /** * Continues the containing thread of this blip.. * * @return an instance of {@link Blip} that represents a the new continuation * reply blip. */ public Blip continueThread() { return operationQueue.continueThreadOfBlip(this); } /** * Inserts an inline blip at the given position. * * @param position the index to insert the inline blip at. This has to be * greater than 0. * @return an instance of {@link Blip} that represents the new inline blip. */ public Blip insertInlineBlip(int position) { if (position <= 0 || position > content.length()) { throw new IllegalArgumentException("Illegal inline blip position: " + position + ". Position has to be greater than 0 and less than or equal to length."); } // Shift the elements. shift(position, 1); content = content.substring(0, position) + " " + content.substring(position); // Generate the operation. Blip inlineBlip = operationQueue.insertInlineBlipToDocument(this, position); // Insert the inline blip element. Element element = new Element(ElementType.INLINE_BLIP); element.setProperty(INLINE_BLIP_ELEMENT_ID_KEY, inlineBlip.getBlipId()); elements.put(position, element); return inlineBlip; } /** * Appends markup ({@code HTML}) content. * * @param markup the markup content to add. */ public void appendMarkup(String markup) { operationQueue.appendMarkupToDocument(this, markup); this.content += convertToPlainText(markup); } /** * Returns a view of this blip that will proxy for the specified id. * * A shallow copy of the current blip 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. * * @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 blip with the proxying information set. */ public Blip proxyFor(String proxyForId) { Util.checkIsValidProxyForId(proxyForId); OperationQueue proxiedOperationQueue = operationQueue.proxyFor(proxyForId); return new Blip(this, proxiedOperationQueue); } /** * Returns the offset of this blip if it is inline, or -1 if it's not. If the * parent is not in the offset, this method will always return -1 since it * can't determine the inline blip status. * * @return the offset of this blip if it is inline, or -1 if it's not inline * or if the parent is not in the context. * @deprecated please use {@code getThread().getLocation()} to get the offset * of the inline reply thread that contains this blip. */ @Deprecated public int getInlineBlipOffset() { Blip parent = getParentBlip(); if (parent == null) { return -1; } for (Entry<Integer, Element> entry : parent.getElements().entrySet()) { Element element = entry.getValue(); if (element.getType() == ElementType.INLINE_BLIP && blipId.equals(element.getProperty(INLINE_BLIP_ELEMENT_ID_KEY))) { return entry.getKey(); } } return -1; } /** * Moves all elements and annotations after the given position by * {@code shiftAmount}. * * @param position the anchor position. * @param shiftAmount the amount to shift the annotations range and elements * position. */ protected void shift(int position, int shiftAmount) { SortedMap<Integer, Element> newElements = new TreeMap<Integer, Element>(elements.headMap(position)); for (Entry<Integer, Element> element : elements.tailMap(position).entrySet()) { newElements.put(element.getKey() + shiftAmount, element.getValue()); } this.elements = newElements; SortedMap<Integer, BlipThread> newInlineReplyThreads = new TreeMap<Integer, BlipThread>(inlineReplyThreads.headMap(position)); for (Entry<Integer, BlipThread> entry : inlineReplyThreads.tailMap(position).entrySet()) { BlipThread thread = entry.getValue(); thread.setLocation(thread.getLocation() + shiftAmount); newInlineReplyThreads.put(thread.getLocation(), thread); } this.inlineReplyThreads = newInlineReplyThreads; this.annotations.shift(position, shiftAmount); } /** * Deletes all annotations that span from {@code start} to {@code end}. * * @param start the start position. * @param end the end position. */ protected void deleteAnnotations(int start, int end) { for (String name : annotations.namesSet()) { annotations.delete(name, start, end); } } /** * Deletes the given blip id from the list of child blip ids. * * @param childBlipId the blip id to delete. */ protected void deleteChildBlipId(String childBlipId) { this.childBlipIds.remove(childBlipId); } /** * Adds the given {@link BlipThread} as a reply or inline reply thread. * * @param thread the new thread to add. */ protected void addThread(BlipThread thread) { if (thread.getLocation() == -1) { this.replyThreads.add(thread); } else { this.inlineReplyThreads.put(thread.getLocation(), thread); } } /** * Removes the given {@link BlipThread} from the reply or inline reply thread. * * @param thread the new thread to remove. */ protected void removeThread(BlipThread thread) { if (thread.getLocation() == -1) { this.replyThreads.remove(thread); } else { this.inlineReplyThreads.remove(thread.getLocation()); } } /** * Converts the given {@code HTML} into robot compatible plaintext. * * @param html the {@code HTML} to convert. * @return a plain text version of the given {@code HTML}. */ private static String convertToPlainText(String html) { StringBuffer result = new StringBuffer(); Matcher matcher = MARKUP_PATTERN.matcher(html); while (matcher.find()) { String replacement = ""; String tag = matcher.group().substring(1, matcher.group().length() - 1).split(" ")[0]; if ("p".equals(tag) || "br".equals(tag)) { replacement = "\n"; } matcher.appendReplacement(result, replacement); } matcher.appendTail(result); return result.toString(); } /** * Serializes this {@link Blip} into a {@link BlipData}. * * @return an instance of {@link BlipData} that represents this blip. */ public BlipData serialize() { BlipData blipData = new BlipData(); // Add primitive properties. blipData.setBlipId(blipId); blipData.setWaveId(ApiIdSerializer.instance().serialiseWaveId(wavelet.getWaveId())); blipData.setWaveletId(ApiIdSerializer.instance().serialiseWaveletId(wavelet.getWaveletId())); blipData.setParentBlipId(parentBlipId); blipData.setThreadId(thread.getId()); blipData.setCreator(creator); blipData.setLastModifiedTime(lastModifiedTime); blipData.setVersion(version); blipData.setContent(content); // Add list and map properties. blipData.setChildBlipIds(childBlipIds); blipData.setContributors(contributors); blipData.setElements(elements); // Add annotations. List<Annotation> annotations = new ArrayList<Annotation>(); for (Annotation annotation : this.annotations) { annotations.add(annotation); } blipData.setAnnotations(annotations); // Add reply threads ids. List<String> replyThreadIds = new ArrayList<String>(inlineReplyThreads.size() + replyThreads.size()); for (BlipThread thread : inlineReplyThreads.values()) { replyThreadIds.add(thread.getId()); } for (BlipThread thread : replyThreads) { replyThreadIds.add(thread.getId()); } blipData.setReplyThreadIds(replyThreadIds); return blipData; } /** * Deserializes the given {@link BlipData} object into an instance of * {@link Blip}. * * @param operationQueue the operation queue. * @param wavelet the wavelet that owns this blip. * @param blipData the blip data to be deserialized. * @return an instance of {@link Wavelet}. */ public static Blip deserialize(OperationQueue operationQueue, Wavelet wavelet, BlipData blipData) { // Extract primitive properties. String blipId = blipData.getBlipId(); String parentBlipId = blipData.getParentBlipId(); String threadId = blipData.getThreadId(); String creator = blipData.getCreator(); long lastModifiedTime = blipData.getLastModifiedTime(); long version = blipData.getVersion(); String content = blipData.getContent(); List<String> childBlipIds = blipData.getChildBlipIds(); List<String> replyThreadIds = blipData.getReplyThreadIds(); if (replyThreadIds == null) { replyThreadIds = new ArrayList<String>(); } List<String> contributors = blipData.getContributors(); Map<Integer, Element> elements = blipData.getElements(); List<Annotation> annotations = blipData.getAnnotations(); return new Blip(blipId, childBlipIds, content, contributors, creator, lastModifiedTime, version, parentBlipId, threadId, annotations, elements, replyThreadIds, wavelet); } }