/** * 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.box.server.robots; import static org.waveprotocol.box.server.robots.util.OperationUtil.buildUserDataWaveletId; import static org.waveprotocol.box.server.robots.util.RobotsUtil.createEmptyRobotWavelet; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.google.wave.api.ApiIdSerializer; import com.google.wave.api.InvalidRequestException; import com.google.wave.api.JsonRpcConstant.ParamsProperty; import com.google.wave.api.JsonRpcResponse; import com.google.wave.api.OperationRequest; import com.google.wave.api.data.converter.EventDataConverter; import com.google.wave.api.event.Event; import com.google.wave.api.event.EventSerializationException; import com.google.wave.api.event.EventSerializer; import com.google.wave.api.event.EventType; import com.google.wave.api.event.OperationErrorEvent; import org.waveprotocol.box.server.frontend.CommittedWaveletSnapshot; import org.waveprotocol.box.server.robots.util.ConversationUtil; import org.waveprotocol.box.server.robots.util.OperationUtil; import org.waveprotocol.box.server.waveserver.WaveServerException; import org.waveprotocol.box.server.waveserver.WaveletProvider; import org.waveprotocol.wave.model.conversation.Conversation; import org.waveprotocol.wave.model.conversation.ConversationBlip; import org.waveprotocol.wave.model.conversation.ObservableConversationView; 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.id.WaveletName; import org.waveprotocol.wave.model.schema.SchemaCollection; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ObservableWaveletData; import org.waveprotocol.wave.model.wave.data.impl.ObservablePluggableMutableDocument; import org.waveprotocol.wave.model.wave.data.impl.WaveletDataImpl; import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet; import org.waveprotocol.wave.util.logging.Log; import java.util.Collections; import java.util.Map; /** * Class which provides context for robot operations and gives access to the * results. * * @author ljvderijk@google.com (Lennard de Rijk) */ public class OperationContextImpl implements OperationContext, OperationResults { private static final Log LOG = Log.get(OperationContextImpl.class); private static final ObservableWaveletData.Factory<? extends ObservableWaveletData> FACTORY = WaveletDataImpl.Factory.create( ObservablePluggableMutableDocument.createFactory(SchemaCollection.empty())); /** * Maps operation ID's to responses. */ private final Map<String, JsonRpcResponse> responses = Maps.newHashMap(); /** * {@link WaveletProvider} that gives us access to wavelets. */ private final WaveletProvider waveletProvider; /** * {@link EventDataConverter} that can convert to * {@link com.google.wave.api.impl.WaveletData} and such. */ private final EventDataConverter converter; /** * The wavelet to which this context is bound, null if unbound. */ private final RobotWaveletData boundWavelet; /** * The wavelets that have been opened in the lifespan of this context. */ private final Map<WaveletName, RobotWaveletData> openedWavelets = Maps.newHashMap(); /** Stores temporary blip ids -> real blip ids */ private final Map<String, String> tempBlipIdMap = Maps.newHashMap(); /** Stores temporary wavelet names -> real wavelet names */ private final Map<WaveletName, WaveletName> tempWaveletNameMap = Maps.newHashMap(); /** Caches {@link ObservableConversationView}s */ private final Map<WaveletName, Map<ParticipantId, ObservableConversationView>> openedConversations; /** Used to create conversations. */ private final ConversationUtil conversationUtil; /** * Constructs an operation context not bound to any wavelet. * * @param waveletProvider the waveletprovider to use for querying wavelet. * @param converter {@link EventDataConverter} for converting from server side * objects. * @param conversationUtil used to create conversations. */ public OperationContextImpl(WaveletProvider waveletProvider, EventDataConverter converter, ConversationUtil conversationUtil) { this(waveletProvider, converter, conversationUtil, null); } /** * Constructs a bound operation context. The bound wavelet is added to the * list of open wavelets. * * @param waveletProvider the waveletprovider to use for querying wavelet. * @param converter {@link EventDataConverter} for converting from server side * objects. * @param boundWavelet the wavelet to bind this context to, null for an * unbound context. * @param conversationUtil used to create conversations. */ public OperationContextImpl(WaveletProvider waveletProvider, EventDataConverter converter, ConversationUtil conversationUtil, RobotWaveletData boundWavelet) { this.waveletProvider = waveletProvider; this.converter = converter; this.conversationUtil = conversationUtil; this.boundWavelet = boundWavelet; this.openedConversations = Maps.newHashMap(); if (boundWavelet != null) { openedWavelets.put(boundWavelet.getWaveletName(), boundWavelet); } } // OperationContext implementation begins here @Override public EventDataConverter getConverter() { return converter; } @Override public boolean isBound() { return boundWavelet != null; } @Override public Map<WaveletName, RobotWaveletData> getOpenWavelets() { return Collections.unmodifiableMap(openedWavelets); } @Override public void constructResponse(OperationRequest operation, Map<ParamsProperty, Object> data) { setResponse(operation.getId(), JsonRpcResponse.result(operation.getId(), data)); } @Override public void constructErrorResponse(OperationRequest operation, String errorMessage) { setResponse(operation.getId(), JsonRpcResponse.error(operation.getId(), errorMessage)); } @Override public void processEvent(OperationRequest operation, Event event) throws InvalidRequestException { // Create JSON-RPC error response. if (event.getType() == EventType.OPERATION_ERROR) { constructErrorResponse(operation, OperationErrorEvent.as(event).getMessage()); return; } // Create JSON-RPC success response. try { constructResponse(operation, EventSerializer.extractPropertiesToParamsPropertyMap(event)); } catch (EventSerializationException e) { LOG.severe("Internal Error occurred, when serializing events", e); throw new InvalidRequestException("Unable to serialize events", operation); } } @Override public void putWavelet(WaveId waveId, WaveletId waveletId, RobotWaveletData newWavelet) { WaveletName waveletName = newWavelet.getWaveletName(); Preconditions.checkArgument(!openedWavelets.containsKey(waveletName), "Not allowed to put an already open wavelet in as a new wavelet"); // New wavelets are indicated by the temporary marker in their waveId. if (waveId.getId().startsWith(TEMP_ID_MARKER)) { tempWaveletNameMap.put(WaveletName.of(waveId, waveletId), waveletName); } openedWavelets.put(waveletName, newWavelet); } @Override public OpBasedWavelet openWavelet(WaveId waveId, WaveletId waveletId, ParticipantId participant) throws InvalidRequestException { WaveletName waveletName; if (waveId.getId().startsWith(TEMP_ID_MARKER)) { WaveletName tempWaveletName = WaveletName.of(waveId, waveletId); waveletName = tempWaveletNameMap.get(tempWaveletName); } else { waveletName = WaveletName.of(waveId, waveletId); } RobotWaveletData wavelet = openedWavelets.get(waveletName); if (wavelet == null) { // Open a wavelet from the server CommittedWaveletSnapshot snapshot = getWaveletSnapshot(waveletName, participant); if (snapshot == null) { if (waveletName.waveletId.equals(buildUserDataWaveletId(participant))) { // Usually the user data is created by the web client whenever user // opens a wavelet for the first time. However, if the wavelet is // fetched for the first time with Robot/Data API - user data should be // created here. wavelet = createEmptyRobotWavelet(participant, waveletName); } else { throw new InvalidRequestException("Wavelet " + waveletName + " couldn't be retrieved"); } } else { ObservableWaveletData obsWavelet = FACTORY.create(snapshot.snapshot); wavelet = new RobotWaveletData(obsWavelet, snapshot.committedVersion); } openedWavelets.put(waveletName, wavelet); } return wavelet.getOpBasedWavelet(participant); } @Override public OpBasedWavelet openWavelet(OperationRequest operation, ParticipantId participant) throws InvalidRequestException { try { WaveId waveId = ApiIdSerializer.instance().deserialiseWaveId( OperationUtil.<String>getRequiredParameter(operation, ParamsProperty.WAVE_ID)); WaveletId waveletId = ApiIdSerializer.instance().deserialiseWaveletId( OperationUtil.<String>getRequiredParameter(operation, ParamsProperty.WAVELET_ID)); return openWavelet(waveId, waveletId, participant); } catch (InvalidIdException e) { throw new InvalidRequestException("Invalid id", operation, e); } } @Override public ObservableConversationView openConversation(WaveId waveId, WaveletId waveletId, ParticipantId participant) throws InvalidRequestException { WaveletName waveletName = WaveletName.of(waveId, waveletId); if (!openedConversations.containsKey(waveletName)) { openedConversations.put( waveletName, Maps.<ParticipantId, ObservableConversationView> newHashMap()); } Map<ParticipantId, ObservableConversationView> conversations = openedConversations.get(waveletName); if (!conversations.containsKey(participant)) { OpBasedWavelet wavelet = openWavelet(waveId, waveletId, participant); conversations.put(participant, conversationUtil.buildConversation(wavelet)); } return conversations.get(participant); } @Override public ObservableConversationView openConversation( OperationRequest operation, ParticipantId participant) throws InvalidRequestException { try { WaveId waveId = ApiIdSerializer.instance().deserialiseWaveId( OperationUtil.<String>getRequiredParameter(operation, ParamsProperty.WAVE_ID)); WaveletId waveletId = ApiIdSerializer.instance().deserialiseWaveletId( OperationUtil.<String>getRequiredParameter(operation, ParamsProperty.WAVELET_ID)); return openConversation(waveId, waveletId, participant); } catch (InvalidIdException e) { throw new InvalidRequestException("Invalid id", operation, e); } } // OperationResults implementation begins here @Override public void putBlip(String blipId, ConversationBlip newBlip) { if (blipId.startsWith(TEMP_ID_MARKER)) { tempBlipIdMap.put(blipId, newBlip.getId()); } } @Override public ConversationBlip getBlip(Conversation conversation, String blipId) throws InvalidRequestException { // We might need to look up the blip id for new blips. String actualBlipId = blipId.startsWith(TEMP_ID_MARKER) ? tempBlipIdMap.get(blipId) : blipId; ConversationBlip blip = conversation.getBlip(actualBlipId); if (blip == null) { throw new InvalidRequestException( "Blip with id " + blipId + " does not exist or has been deleted"); } return blip; } // OperationResults implementation begins here @Override public ConversationUtil getConversationUtil() { return conversationUtil; } @Override public Map<String, JsonRpcResponse> getResponses() { return Collections.unmodifiableMap(responses); } @Override public JsonRpcResponse getResponse(String operationId) { return responses.get(operationId); } @Override public boolean hasResponse(String operationId) { return responses.containsKey(operationId); } /** * Stores a response in this context. * * @param operationId the id of the robot operation. * @param response the response to store. */ private void setResponse(String operationId, JsonRpcResponse response) { Preconditions.checkState( !responses.containsKey(operationId), "Overwriting an existing response"); responses.put(operationId, response); } /** * Takes snapshot of a wavelet, checking access for the given participant. * * @return snapshot on success, null on failure */ private CommittedWaveletSnapshot getWaveletSnapshot(WaveletName waveletName, ParticipantId participant) { try { if (!waveletProvider.checkAccessPermission(waveletName, participant)) { LOG.severe( participant + " tried to open " + waveletName + " which it isn't participating in"); return null; } return waveletProvider.getSnapshot(waveletName); } catch (WaveServerException e) { LOG.severe("Cannot access wavelet " + waveletName, e); return null; } } }