/* * * Copyright (C) 2007-2015 Licensed to the Comunes Association (CA) under * one or more contributor license agreements (see COPYRIGHT for details). * The CA licenses this file to you under the GNU Affero General Public * License version 3, (the "License"); you may not use this file except in * compliance with the License. This file is part of kune. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package cc.kune.wave.server.kspecific; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nonnull; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.waveprotocol.box.server.CoreSettings; import org.waveprotocol.box.server.robots.OperationContextImpl; import org.waveprotocol.box.server.robots.OperationServiceRegistry; import org.waveprotocol.box.server.robots.util.ConversationUtil; import org.waveprotocol.box.server.robots.util.LoggingRequestListener; import org.waveprotocol.box.server.robots.util.OperationUtil; import org.waveprotocol.box.server.waveserver.WaveletProvider; import org.waveprotocol.box.server.waveserver.WaveletProvider.SubmitRequestListener; import org.waveprotocol.wave.model.id.InvalidIdException; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.waveref.WaveRef; import cc.kune.common.shared.utils.SimpleArgCallback; import cc.kune.common.shared.utils.TextUtils; import cc.kune.core.client.errors.AccessViolationException; import cc.kune.core.client.errors.DefaultException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.name.Named; import com.google.wave.api.Annotation; import com.google.wave.api.ApiIdSerializer; import com.google.wave.api.Blip; import com.google.wave.api.BlipContent; import com.google.wave.api.BlipData; import com.google.wave.api.BlipThread; import com.google.wave.api.Element; import com.google.wave.api.ElementType; import com.google.wave.api.Gadget; import com.google.wave.api.JsonRpcConstant.ParamsProperty; import com.google.wave.api.JsonRpcResponse; import com.google.wave.api.OperationQueue; import com.google.wave.api.OperationRequest; import com.google.wave.api.OperationRequest.Parameter; import com.google.wave.api.Participants; import com.google.wave.api.ProtocolVersion; import com.google.wave.api.Range; import com.google.wave.api.Wavelet; import com.google.wave.api.data.converter.EventDataConverterManager; import com.google.wave.api.impl.DocumentModifyAction; import com.google.wave.api.impl.DocumentModifyAction.BundledAnnotation; import com.google.wave.api.impl.DocumentModifyAction.ModifyHow; import com.google.wave.api.impl.DocumentModifyQuery; import com.google.wave.api.impl.WaveletData; import com.google.wave.splash.rpc.ClientAction; import com.google.wave.splash.web.template.WaveRenderer; public class KuneWaveServiceDefault implements KuneWaveService { public static final Log LOG = LogFactory.getLog(KuneWaveServiceDefault.class); // See: DocumentModifyServiceTest private static final String NO_ANNOTATION_KEY = null; private static final List<BundledAnnotation> NO_BUNDLED_ANNOTATIONS = Collections.emptyList(); private static final List<String> NO_VALUES = Collections.<String> emptyList(); /** * * Copy blips * * @param fromBlip * @param toBlip * * @author yurize@apache.org (Yuri Zelikov) * */ public static void copyWavelet(final Blip fromBlip, final Blip toBlip) { for (final BlipContent blipContent : fromBlip.all().values()) { toBlip.append(blipContent); } // Deep copy annotations for (final Annotation annotation : fromBlip.getAnnotations()) { final Range range = annotation.getRange(); try { toBlip.range(range.getStart() + 1, range.getEnd() + 1).annotate(annotation.getName(), annotation.getValue()); } catch (final IndexOutOfBoundsException e) { // Don't copy faulty annotations } } } public static void copyWaveletElements(final Blip fromBlip, final Blip toBlip) { // Deep copy form elements. // DocumentModifyService don't permit this: // "Can't insert other elements than text and gadgets at the moment"); for (final Entry<Integer, Element> entry : fromBlip.getElements().entrySet()) { final ElementType type = entry.getValue().getType(); Element result = null; // if (FormElement.getFormElementTypes().contains(type)) { // result = new FormElement(type, entry.getValue().getProperties()); // } else if (type == ElementType.GADGET) { result = new Gadget(entry.getValue().getProperties()); // } else if (type == ElementType.IMAGE) { // result = new Image(entry.getValue().getProperties()); // } else if (type == ElementType.LINE) { // result = new Line(entry.getValue().getProperties()); // } else { // result = new Element(type, entry.getValue().getProperties()); } if (result != null) { toBlip.append(result); } } } private final ConversationUtil conversationUtil; private final EventDataConverterManager converterManager; private final String domain; private final OperationServiceRegistry operationRegistry; private final ParticipantUtils participantUtils; private final WaveletProvider waveletProvider; private final WaveRenderer waveRenderer; @Inject public KuneWaveServiceDefault(final EventDataConverterManager converterManager, @Named("DataApiRegistry") final OperationServiceRegistry operationRegistry, final WaveletProvider waveletProvider, final ConversationUtil conversationUtil, final ParticipantUtils participantUtils, final WaveRenderer waveRenderer, @Named(CoreSettings.WAVE_SERVER_DOMAIN) final String domain) { this.converterManager = converterManager; this.waveletProvider = waveletProvider; this.conversationUtil = conversationUtil; this.operationRegistry = operationRegistry; this.participantUtils = participantUtils; this.waveRenderer = waveRenderer; this.domain = domain; } @Override public void addGadget(final WaveRef waveName, final String author, final URL gadgetUrl) { // See DocumentModifyServiceTest final List<Element> elementsIn = Lists.newArrayListWithCapacity(1); final Map<String, String> properties = Maps.newHashMap(); properties.put(Gadget.URL, gadgetUrl.toString()); properties.put(Gadget.AUTHOR, participantUtils.of(author).getAddress()); final Gadget gadget = new Gadget(properties); elementsIn.add(gadget); final Wavelet wavelet = fetchWave(waveName, author); final OperationQueue opQueue = new OperationQueue(); final Blip rootBlip = wavelet.getRootBlip(); final OperationRequest operationRequest = opQueue.modifyDocument(rootBlip); operationRequest.addParameter(Parameter.of(ParamsProperty.MODIFY_ACTION, new DocumentModifyAction( ModifyHow.INSERT, NO_VALUES, NO_ANNOTATION_KEY, elementsIn, NO_BUNDLED_ANNOTATIONS, false))); operationRequest.addParameter(Parameter.of(ParamsProperty.INDEX, 1)); doOperations(author, opQueue, "add gadget"); } @Override public boolean addParticipants(final WaveRef waveName, final String author, final String userWhoAdds, final String... newLocalParticipants) { boolean added = false; final Wavelet wavelet = fetchWave(waveName, author); final Participants currentParticipants = wavelet.getParticipants(); final String whoAdd = wavelet.getParticipants().contains(participantUtils.of(userWhoAdds)) ? userWhoAdds : author; final OperationQueue opQueue = new OperationQueue(); for (final String participant : participantUtils.toSet(newLocalParticipants)) { final String newPartWithDomain = participantUtils.of(participant).toString(); // Removing duplicates if (!currentParticipants.contains(newPartWithDomain)) { // FIXME This is very costly. Seems like only one participant per // opQueue is added (try to // fix this in WAVE) LOG.debug("Adding as participant: " + newPartWithDomain); opQueue.addParticipantToWavelet(wavelet, newPartWithDomain); added = true; } } doOperations(whoAdd, opQueue, "add participant"); return added; } @Override public WaveRef createWave(final String message, final SimpleArgCallback<WaveRef> onCreate, final ParticipantId... participants) { return createWave(NO_TITLE, message, onCreate, participants); } @Override public WaveRef createWave(@Nonnull final String title, final String message, final SimpleArgCallback<WaveRef> onCreate, @Nonnull final ParticipantId... participantsArray) { return createWave(title, message, onCreate, WITHOUT_GADGET, participantsArray); } @Override public WaveRef createWave(final String title, final String message, final SimpleArgCallback<WaveRef> onCreate, final String... participantsArray) { return createWave(title, message, onCreate, participantUtils.listFrom(participantsArray)); } @Override public WaveRef createWave(final String title, final String message, final SimpleArgCallback<WaveRef> onCreate, final URL gadgetUrl, final Map<String, String> gadgetProperties, final ParticipantId... participantsArray) { return createWave(title, message, NO_WAVE_TO_COPY, onCreate, gadgetUrl, gadgetProperties, participantsArray); } @Override public WaveRef createWave(@Nonnull final String title, final String message, final SimpleArgCallback<WaveRef> onCreate, final URL gadgetUrl, @Nonnull final ParticipantId... participantsArray) { return createWave(title, message, NO_WAVE_TO_COPY, onCreate, gadgetUrl, participantsArray); } @Override public WaveRef createWave(@Nonnull final String title, final String message, final WaveRef waveIdToCopy, final SimpleArgCallback<WaveRef> onCreate, final URL gadgetUrl, final Map<String, String> gadgetProperties, @Nonnull final ParticipantId... participantsArray) { String newWaveId = null; String newWaveletId = null; final Set<String> participants = new HashSet<String>(); for (final ParticipantId participant : participantsArray) { participants.add(participant.toString()); } final ParticipantId user = participantsArray[0]; final OperationQueue opQueue = new OperationQueue(); final Wavelet newWavelet = opQueue.createWavelet(domain, participants); opQueue.setTitleOfWavelet(newWavelet, title); final Blip rootBlip = newWavelet.getRootBlip(); rootBlip.append(new com.google.wave.api.Markup(message).getText()); if (waveIdToCopy != NO_WAVE_TO_COPY) { try { // WaveId copyWaveId; // copyWaveId = WaveId.ofChecked(domain, waveIdToCopy); final Wavelet waveletToCopy = fetchWave(waveIdToCopy.getWaveId(), waveIdToCopy.getWaveletId(), participantsArray[0].toString()); if (waveletToCopy != null) { copyWavelet(waveletToCopy.getRootBlip(), rootBlip); // copyWaveletElements(waveletToCopy.getRootBlip(), rootBlip); } } catch (final DefaultException e2) { LOG.error("Error copying wave content", e2); } } if (gadgetUrl != WITHOUT_GADGET) { Gadget gadget; if (gadgetProperties.size() > 0) { gadgetProperties.put(Gadget.URL, gadgetUrl.toString()); gadget = new Gadget(gadgetProperties); } else { gadget = new Gadget(gadgetUrl.toString()); } rootBlip.append(gadget); } final OperationContextImpl context = new OperationContextImpl(waveletProvider, converterManager.getEventDataConverter(ProtocolVersion.DEFAULT), conversationUtil); for (final OperationRequest req : opQueue.getPendingOperations()) { OperationUtil.executeOperation(req, operationRegistry, context, user); final String reqId = req.getId(); final JsonRpcResponse response = context.getResponse(reqId); if (response != null) { if (response.isError()) { final String errorMessage = processErrorMessage(context.getResponse(reqId).getErrorMessage()); onFailure(errorMessage); throw new DefaultException(errorMessage); } else { final Object responseWaveId = response.getData().get(ParamsProperty.WAVE_ID); final Object responseWaveletId = response.getData().get(ParamsProperty.WAVELET_ID); if (responseWaveId != null && responseWaveletId != null) { // This is serialized use // ApiIdSerializer.instance().deserialiseWaveId (see // WaveService) newWaveId = (String) responseWaveId; newWaveletId = (String) responseWaveletId; } } } } WaveRef wavename; try { wavename = WaveRef.of(ApiIdSerializer.instance().deserialiseWaveId(newWaveId), ApiIdSerializer.instance().deserialiseWaveletId(newWaveletId)); } catch (final InvalidIdException e) { throw new DefaultException("Error getting wave id"); } doSubmit(onCreate, context, wavename); LOG.info("WaveId: " + newWaveId + " waveletId: " + newWaveletId); return wavename; } @Override public WaveRef createWave(@Nonnull final String title, final String message, final WaveRef waveIdToCopy, final SimpleArgCallback<WaveRef> onCreate, final URL gadgetUrl, @Nonnull final ParticipantId... participantsArray) { return createWave(title, message, waveIdToCopy, onCreate, gadgetUrl, Collections.<String, String> emptyMap(), participantsArray); } @Override public boolean delParticipants(final WaveRef waveName, final String whoDel, final Set<String> participants) { boolean removed = false; final Wavelet wavelet = fetchWave(waveName, whoDel); final Participants currentParticipants = wavelet.getParticipants(); LOG.debug("Removing participants: " + participants.toString()); final OperationQueue opQueue = new OperationQueue(); for (final String participant : participants) { // FIXME Seems like only one participant per opQueue is added (try to fix // this in WAVE) final String partWithDomain = participantUtils.of(participant).toString(); if (currentParticipants.contains(partWithDomain)) { LOG.debug("Removing as participant: " + partWithDomain); removed = true; opQueue.removeParticipantFromWavelet(wavelet, partWithDomain); } } doOperations(whoDel, opQueue, "del participant"); return removed; } @Override public boolean delParticipants(final WaveRef waveName, final String whoDel, final String... participants) { return delParticipants(waveName, whoDel, participantUtils.toSet(participants)); } // final SubmitRequestListener listener private void doOperations(final String author, final OperationQueue opQueue, final String logComment) { final OperationContextImpl context = new OperationContextImpl(waveletProvider, converterManager.getEventDataConverter(ProtocolVersion.DEFAULT), conversationUtil); for (final OperationRequest req : opQueue.getPendingOperations()) { OperationUtil.executeOperation(req, operationRegistry, context, participantUtils.of(author)); } OperationUtil.submitDeltas(context, waveletProvider, new LoggingRequestListener( org.waveprotocol.wave.util.logging.Log.get(KuneWaveServiceDefault.class))); } private void doSubmit(final SimpleArgCallback<WaveRef> onCreate, final OperationContextImpl context, final WaveRef wavename) { OperationUtil.submitDeltas(context, waveletProvider, new SubmitRequestListener() { @Override public void onFailure(final String arg0) { KuneWaveServiceDefault.this.onFailure("Wave creation failed, onFailure: " + arg0); } @Override public void onSuccess(final int arg0, final HashedVersion arg1, final long arg2) { LOG.info("Wave creation success: " + arg1); onCreate.onCallback(wavename); } }); } @Override public Wavelet fetchWave(final WaveId waveId, final WaveletId waveletId, final String author) { final OperationQueue opQueue = new OperationQueue(); opQueue.fetchWavelet(waveId, waveletId); Wavelet wavelet = null; final OperationContextImpl context = new OperationContextImpl(waveletProvider, converterManager.getEventDataConverter(ProtocolVersion.DEFAULT), conversationUtil); final OperationRequest request = opQueue.getPendingOperations().get(0); OperationUtil.executeOperation(request, operationRegistry, context, participantUtils.of(author)); final String reqId = request.getId(); final JsonRpcResponse response = context.getResponse(reqId); if (response != null && response.isError()) { final String errorMessage = processErrorMessage(context.getResponse(reqId).getErrorMessage()); onFailure(errorMessage); if ("Access rejected".equals(errorMessage)) { throw new AccessViolationException(errorMessage); } else { throw new DefaultException(errorMessage); } } else { // Duplicate code from WaveService assert response != null; final WaveletData waveletData = (WaveletData) response.getData().get(ParamsProperty.WAVELET_DATA); final Map<String, Blip> blips = new HashMap<String, Blip>(); final Map<String, BlipThread> threads = new HashMap<String, BlipThread>(); wavelet = Wavelet.deserialize(opQueue, blips, threads, waveletData); // Deserialize threads. @SuppressWarnings("unchecked") final Map<String, BlipThread> tempThreads = (Map<String, BlipThread>) response.getData().get( ParamsProperty.THREADS); for (final Map.Entry<String, BlipThread> entry : tempThreads.entrySet()) { final BlipThread thread = entry.getValue(); threads.put(entry.getKey(), new BlipThread(thread.getId(), thread.getLocation(), thread.getBlipIds(), blips)); } // Deserialize blips. @SuppressWarnings("unchecked") final Map<String, BlipData> blipDatas = (Map<String, BlipData>) response.getData().get( ParamsProperty.BLIPS); for (final Map.Entry<String, BlipData> entry : blipDatas.entrySet()) { blips.put(entry.getKey(), Blip.deserialize(opQueue, wavelet, entry.getValue())); } } return wavelet; } @Override public Wavelet fetchWave(final WaveRef waveName, final String author) { final WaveId waveId = waveName.getWaveId(); final WaveletId waveletId = waveName.getWaveletId(); return fetchWave(waveId, waveletId, author); } @Override public Gadget getGadget(final WaveRef waveletName, final String author, final URL gadgetUrl) { final Wavelet wavelet = fetchWave(waveletName, author); final Blip rootBlip = wavelet.getRootBlip(); for (final Element elem : rootBlip.getElements().values()) { if (elem.isGadget()) { final Map<String, String> properties = elem.getProperties(); if (properties.get(Gadget.URL).equals(gadgetUrl.toString())) { return (Gadget) elem; } } } return null; } @Override public Participants getParticipants(final WaveRef waveref, final String author) { return fetchWave(waveref, author).getParticipants(); } @Override public String getTitle(final WaveRef waveName, final String author) { final Wavelet wavelet = fetchWave(waveName, author); return wavelet.getTitle(); } @Override public boolean isParticipant(final Wavelet wavelet, final String user) { return wavelet.getParticipants().contains(participantUtils.of(user).toString()); } private void onFailure(final String message) { LOG.error(message); } private String processErrorMessage(final String message) { final String errorMsg = TextUtils.notEmpty(message) ? message : "Wave operation failed"; return errorMsg; } @Override public String render(final Wavelet wavelet) { final ClientAction clientPage = waveRenderer.render(wavelet, 0); final String html = clientPage.getHtml(); return html; } @Override public String render(final WaveRef waveRef, final String author) { return render(fetchWave(waveRef, author)); } @Override public void setGadgetProperty(final WaveRef waveletName, final String author, final URL gadgetUrl, final Map<String, String> newProperties) { // Note: See BlipContentRefs DocumentModifyService final Wavelet wavelet = fetchWave(waveletName, author); final Blip rootBlip = wavelet.getRootBlip(); for (final Element elem : rootBlip.getElements().values()) { if (elem.isGadget()) { final Map<String, String> properties = elem.getProperties(); if (properties.get(Gadget.URL).equals(gadgetUrl.toString())) { // This is the gadget we want to modify (the first of that type) final List<Element> updatedElementsIn = Lists.newArrayListWithCapacity(1); final Gadget gadget = (Gadget) elem; final OperationQueue opQueue = new OperationQueue(); for (final String propKey : newProperties.keySet()) { final String value = newProperties.get(propKey); properties.put(propKey, value); // properties.put(propertyNameToDelete, null); gadget.setProperty(propKey, value); // updatedElementsIn.add(new Gadget(properties)); } updatedElementsIn.add(gadget); final OperationRequest operationRequest = opQueue.modifyDocument(rootBlip); operationRequest.addParameter(Parameter.of(ParamsProperty.MODIFY_ACTION, new DocumentModifyAction(ModifyHow.UPDATE_ELEMENT, NO_VALUES, NO_ANNOTATION_KEY, updatedElementsIn, NO_BUNDLED_ANNOTATIONS, false))); operationRequest.addParameter(Parameter.of( ParamsProperty.MODIFY_QUERY, new DocumentModifyQuery(ElementType.GADGET, ImmutableMap.of(Gadget.URL, gadgetUrl.toString()), -1))); doOperations(author, opQueue, "set gadget property"); break; } } } } @Override public void setTitle(final WaveRef waveName, final String title, final String author) { final Wavelet wavelet = fetchWave(waveName, author); final OperationQueue opQueue = new OperationQueue(); opQueue.setTitleOfWavelet(wavelet, title); doOperations(author, opQueue, "set title"); } }