/** * Copyright 2010 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.data.converter.v21; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.google.wave.api.Annotation; import com.google.wave.api.ApiIdSerializer; import com.google.wave.api.BlipData; import com.google.wave.api.Range; import com.google.wave.api.data.ApiView; import com.google.wave.api.data.ElementSerializer; import com.google.wave.api.data.converter.EventDataConverter; import com.google.wave.api.impl.EventMessageBundle; import com.google.wave.api.impl.WaveletData; import org.waveprotocol.wave.model.account.DocumentBasedRoles; import org.waveprotocol.wave.model.conversation.Conversation; import org.waveprotocol.wave.model.conversation.ConversationBlip; import org.waveprotocol.wave.model.conversation.ConversationThread; import org.waveprotocol.wave.model.conversation.TagsDocument; import org.waveprotocol.wave.model.conversation.TitleHelper; import org.waveprotocol.wave.model.document.Document; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.RangedAnnotation; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.id.IdConstants; import org.waveprotocol.wave.model.id.IdUtil; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.Wavelet; import java.util.Collection; import java.util.Iterator; import java.util.List; /** * An implementation of {@link EventDataConverter} for all protocol versions * that are less than or equal to v0.21. * */ public class EventDataConverterV21 implements EventDataConverter { @Override public WaveletData toWaveletData(Wavelet wavelet, Conversation conversation, EventMessageBundle eventMessageBundle) { final WaveletData waveletData = new WaveletData(); waveletData.setCreationTime(wavelet.getCreationTime()); waveletData.setCreator(wavelet.getCreatorId().getAddress()); waveletData.setWaveId(ApiIdSerializer.instance().serialiseWaveId(wavelet.getWaveId())); waveletData.setWaveletId(ApiIdSerializer.instance().serialiseWaveletId(wavelet.getId())); waveletData.setLastModifiedTime(wavelet.getLastModifiedTime()); waveletData.setParticipants(idsToParticipantIdList(wavelet.getParticipantIds())); waveletData.setRootBlipId(conversation.getRootThread().getFirstBlip().getId()); waveletData.setTitle(getTitle(wavelet, conversation)); waveletData.setVersion(wavelet.getVersion()); // Add Data Docs. All data documents are silently name spaced under the // robot prefix to avoid conflicts. Any docId containing a '+' will be // ignored for now. for (String documentId : wavelet.getDocumentIds()) { if (IdUtil.isRobotDocId(documentId)) { String[] parts = IdUtil.split(documentId); if (parts.length == 2) { Document document = wavelet.getDocument(documentId); String val = XmlStringBuilder.innerXml(document).getXmlString(); waveletData.setDataDocument(parts[1], val); } } } // Add the tags. if (wavelet.getDocument(IdConstants.TAGS_DOC_ID) != null) { @SuppressWarnings("unchecked") TagsDocument tags = new TagsDocument(wavelet.getDocument(IdConstants.TAGS_DOC_ID)); tags.addListener(new TagsDocument.Listener() { @Override public void onAdd(String tagName) { waveletData.addTag(tagName); } @Override public void onRemove(int tagPosition) { // Not called. }}); tags.processInitialState(); } // Add the participant roles. ObservableDocument rolesDocument = wavelet.getDocument(IdConstants.ROLES_DATA_DOC_ID); if (rolesDocument != null) { DocumentBasedRoles roles = DocumentBasedRoles.create(rolesDocument); for (ParticipantId participantId : wavelet.getParticipantIds()) { waveletData.setParticipantRole(participantId.getAddress(), roles.getRole(participantId).name()); } } return waveletData; } @Override public BlipData toBlipData(ConversationBlip blip, Wavelet wavelet, EventMessageBundle eventMessageBundle) { ConversationBlip parentBlip = findBlipParent(blip); BlipData blipData = new BlipData(); blipData.setCreator(blip.getAuthorId().getAddress()); blipData.setContributors(idsToParticipantIdList(blip.getContributorIds())); blipData.setBlipId(blip.getId()); blipData.setLastModifiedTime(blip.getLastModifiedTime()); blipData.setVersion(blip.getLastModifiedVersion()); blipData.setParentBlipId(parentBlip == null ? null : parentBlip.getId()); blipData.setWaveId(ApiIdSerializer.instance().serialiseWaveId(wavelet.getWaveId())); blipData.setWaveletId(ApiIdSerializer.instance().serialiseWaveletId(wavelet.getId())); blipData.setChildBlipIds(toBlipIdList(findBlipChildren(blip))); ApiView apiView = new ApiView(blip.getContent(), wavelet); // Set content. blipData.setContent(apiView.apiContents()); // Set Annotations. blipData.setAnnotations(extractAnnotations(blip.getContent(), apiView)); // blip.getContent().rangedAnnotations(0, blip.getContent().size(), null), // Set Form Elements. blipData.setElements(ElementSerializer.serialize(blip.getContent(), wavelet)); return blipData; } /** * Finds the children of a blip, defined as the next sibling blip and the * first blip in each reply thread. * * @param blip the blip. * @return the children of the given blip. */ @Override public List<ConversationBlip> findBlipChildren(ConversationBlip blip) { List<ConversationBlip> siblings = Lists.newArrayList(); ConversationBlip nextSibling = findNextSibling(blip); if (nextSibling != null) { siblings.add(nextSibling); } for (ConversationThread reply : blip.getReplyThreads()) { if (reply.getFirstBlip() != null) { siblings.add(reply.getFirstBlip()); } } return siblings; } /** * Finds the parent of a blip. The parent is the preceding blip in the thread, * or the blip to which the thread is a reply for the first blip in a thread. * The first blip of the root thread has no parent. * * @param blip the blip. * @return the blip's parent, or {@code null} if the blip is the first blip * in a conversation. */ @Override public ConversationBlip findBlipParent(ConversationBlip blip) { ConversationThread containingThread = blip.getThread(); if (containingThread.getFirstBlip() == blip && containingThread != blip.getConversation().getRootThread()) { return containingThread.getParentBlip(); } return findPreviousSibling(blip); } /** * Converts a collection of {@link ParticipantId}s to a list of addresses. * * @param participantIds the participant ids to convert. * @return a list of addresses. */ public List<String> idsToParticipantIdList(Collection<ParticipantId> participantIds) { List<String> addresses = Lists.newArrayListWithCapacity(participantIds.size()); for (ParticipantId id : participantIds) { addresses.add(id.getAddress()); } return addresses; } /** * Finds the previous sibling of a blip in a thread. The first blip in a * thread has no previous sibling. * * @param blip the blip. * @return the previous sibling of the blip, or {@code null}. */ @VisibleForTesting static ConversationBlip findPreviousSibling(ConversationBlip blip) { ConversationThread thread = blip.getThread(); ConversationBlip previous = null; for (ConversationBlip sibling : thread.getBlips()) { if (sibling == blip) { break; } previous = sibling; } return previous; } /** * Finds the next sibling of a blip in a thread. The last blip in a thread has * no next sibling. * * @param blip the blip. * @return the next sibling of the blip, or {@code null} if blip is the last * blip in a thread. */ @VisibleForTesting static ConversationBlip findNextSibling(ConversationBlip blip) { ConversationThread thread = blip.getThread(); Iterator<? extends ConversationBlip> blips = thread.getBlips().iterator(); boolean foundBlip = false; while (!foundBlip && blips.hasNext()) { if (blips.next() == blip) { foundBlip = true; } } return blips.hasNext() ? blips.next() : null; } /** * Retrieves the title of a {@link Wavelet}. * * @param wavelet The {@link Wavelet} to retrieve the title from. * @param conversation The wavelet conversation * @return the title of the {@link Wavelet}, or an empty string if it has * no title. */ private static String getTitle(Wavelet wavelet, Conversation conversation) { ConversationThread rootThread = conversation.getRootThread(); if (rootThread == null) { return ""; } ConversationBlip firstBlip = rootThread.getFirstBlip(); if (firstBlip == null) { return ""; } Document doc = firstBlip.getContent(); return TitleHelper.extractTitle(doc); } /** * Extracts the blip ids of the given list of blips. * * @param children the blips. * @return the blip ids of the blips. */ private static List<String> toBlipIdList(List<ConversationBlip> children) { List<String> ids = Lists.newArrayListWithCapacity(children.size()); for (ConversationBlip child : children) { ids.add(child.getId()); } return ids; } /** * Extracts all annotations that span inside the body tag of the given * document. * * @param doc the document to get the annotations from. * @param apiView provides a utility function to convert an xml offset point * into text offset. * @return the annotations represented as a list of {@link Annotation}. */ private static List<Annotation> extractAnnotations(Document doc, ApiView apiView) { List<Annotation> result = Lists.newArrayList(); for (RangedAnnotation<String> annotation : doc.rangedAnnotations(0, doc.size(), null)) { if (annotation.key() != null && annotation.value() != null) { int start = apiView.transformToTextOffset(annotation.start()); int end = apiView.transformToTextOffset(annotation.end()); result.add(new Annotation(annotation.key(), annotation.value(), new Range(start, end))); } } return result; } }