/** * 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 org.waveprotocol.examples.robots.echoey; import com.google.common.base.Preconditions; import com.google.common.collect.MapMaker; import com.google.wave.api.AbstractRobot; import com.google.wave.api.Annotation; import com.google.wave.api.Blip; import com.google.wave.api.Context; import com.google.wave.api.Wavelet; import com.google.wave.api.event.AnnotatedTextChangedEvent; import com.google.wave.api.event.DocumentChangedEvent; import com.google.wave.api.event.WaveletBlipCreatedEvent; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Robot that echoes the changes to a wave. * * @author ljvderijk@google.com (Lennard de Rijk) */ public class Echoey extends AbstractRobot { /** Annotation for text by Echoey */ private static final String ECHOEY_ANNOTATION = "www.waveprotocol.org/echoey"; private final Map<String, String> shadowBlipMap; public Echoey() { shadowBlipMap = new MapMaker().expireAfterWrite(1, TimeUnit.HOURS).makeMap(); } @Override protected String getRobotName() { return "Echoey"; } @Override protected String getRobotProfilePageUrl() { return "http://www.waveprotocol.org/"; } @Capability(contexts = {Context.SELF, Context.SIBLINGS}) @Override public void onWaveletBlipCreated(WaveletBlipCreatedEvent event) { Blip blip = event.getNewBlip(); if (!isShadowBlip(blip)) { createShadowBlip(blip); } } @Capability(contexts = {Context.SELF, Context.SIBLINGS}) @Override public void onDocumentChanged(DocumentChangedEvent event) { Blip blip = event.getBlip(); if (!isShadowBlip(blip)) { createOrUpdateShadowBlip(blip); } } @Capability(contexts = {Context.SELF, Context.SIBLINGS}) @Override public void onAnnotatedTextChanged(AnnotatedTextChangedEvent event) { Blip blip = event.getBlip(); if (!isShadowBlip(blip)) { createOrUpdateShadowBlip(blip); } } /** * Creates a new shadow blip. * * Note that this method will not know the id of the new blip since the * server will generate it. * * @param blip the blip to create a new shadow blip for. */ private void createShadowBlip(Blip blip) { Blip newBlip = blip.continueThread(); newBlip.all().replace(blip.getContent()); newBlip.all().annotate(ECHOEY_ANNOTATION, blip.getBlipId()); } /** * Creates or updates an existing shadow blip that should shadow the given * blip. * * @param blipToShadow the blip that should be shadowed. */ private void createOrUpdateShadowBlip(Blip blipToShadow) { Wavelet wavelet = blipToShadow.getWavelet(); String blipId = blipToShadow.getBlipId(); if (shadowBlipMap.containsKey(blipId)) { Blip shadowBlip = wavelet.getBlip(shadowBlipMap.get(blipId)); updateShadowBlip(shadowBlip, blipToShadow); } else { updateShadowMap(wavelet); if (!shadowBlipMap.containsKey(blipId)) { createShadowBlip(blipToShadow); } else { // Update existing shadow Blip Blip shadowBlip = wavelet.getBlip(shadowBlipMap.get(blipId)); updateShadowBlip(shadowBlip, blipToShadow); } } } /** * Updates an existing shadow blip. * * @param shadowBlip the blip that is shadowing. * @param blipToShadow the blip being shadowed. */ private void updateShadowBlip(Blip shadowBlip, Blip blipToShadow) { Preconditions.checkNotNull(shadowBlip, "Shadow blip can't be null"); Preconditions.checkNotNull(blipToShadow, "Blip to shadow can't be null"); shadowBlip.all().replace(blipToShadow.getContent()); for (Annotation annotation : blipToShadow.getAnnotations()) { if (annotation.getName().equals(ECHOEY_ANNOTATION)) { continue; } shadowBlip.range(annotation.getRange().getStart(), annotation.getRange().getEnd()).annotate( annotation.getName(), annotation.getValue()); } shadowBlip.all().annotate(ECHOEY_ANNOTATION, blipToShadow.getBlipId()); shadowBlipMap.put(blipToShadow.getBlipId(), shadowBlip.getBlipId()); } /** * Updates the shadow map for a given wavelet. */ private void updateShadowMap(Wavelet wavelet) { for (Blip blip : wavelet.getBlips().values()) { if (isShadowBlip(blip)) { String shadowBlipId = blip.getBlipId(); if (!shadowBlipMap.containsValue(shadowBlipId)) { String originalBlipId = blip.getAnnotations().get(ECHOEY_ANNOTATION).get(0).getValue(); shadowBlipMap.put(originalBlipId, shadowBlipId); } } } } /** * Return true if the given blip is a blip in which Echoey has written. */ private boolean isShadowBlip(Blip blip) { List<Annotation> annotations = blip.getAnnotations().get(ECHOEY_ANNOTATION); return annotations != null && !annotations.isEmpty(); } }