/** * 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.passive; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.name.Named; import com.google.wave.api.RobotSerializer; import com.google.wave.api.data.converter.EventDataConverterManager; import com.google.wave.api.robot.CapabilityFetchException; import com.google.wave.api.robot.RobotName; import org.waveprotocol.box.common.DeltaSequence; import org.waveprotocol.box.server.account.AccountData; import org.waveprotocol.box.server.account.RobotAccountData; import org.waveprotocol.box.server.persistence.AccountStore; import org.waveprotocol.box.server.persistence.PersistenceException; import org.waveprotocol.box.server.robots.operations.NotifyOperationService; import org.waveprotocol.box.server.robots.util.ConversationUtil; import org.waveprotocol.box.server.waveserver.WaveBus; import org.waveprotocol.box.server.waveserver.WaveletProvider; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.wave.AddParticipant; import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta; import org.waveprotocol.wave.model.operation.wave.WaveletOperation; import org.waveprotocol.wave.model.version.HashedVersion; import org.waveprotocol.wave.model.wave.ParticipantId; import org.waveprotocol.wave.model.wave.data.ReadableWaveletData; import org.waveprotocol.wave.util.logging.Log; import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; /** * Gateway for the Passive Robot API, this class can be subscribed to the * WaveBus and fires of separate threads to handle any updates for Robots. * * @author ljvderijk@google.com (Lennard de Rijk) */ public class RobotsGateway implements WaveBus.Subscriber { private static final Log LOG = Log.get(RobotsGateway.class); private final WaveletProvider waveletProvider; private final AccountStore accountStore; private final EventDataConverterManager converterManager; private final RobotConnector connector; private final Map<RobotName, Robot> allRobots = Maps.newHashMap(); private final Set<RobotName> runnableRobots = Sets.newHashSet(); private final Executor executor; private final ConversationUtil conversationUtil; private final NotifyOperationService notifyOpService; @Inject @VisibleForTesting RobotsGateway(WaveletProvider waveletProvider, RobotConnector connector, AccountStore accountStore, RobotSerializer serializer, EventDataConverterManager converterManager, @Named("GatewayExecutor") Executor executor, ConversationUtil conversationUtil, NotifyOperationService notifyOpService) { this.waveletProvider = waveletProvider; this.accountStore = accountStore; this.converterManager = converterManager; this.connector = connector; this.executor = executor; this.conversationUtil = conversationUtil; this.notifyOpService = notifyOpService; } @Override public void waveletCommitted(WaveletName waveletName, HashedVersion version) { // We ignore this event. } @Override public void waveletUpdate(ReadableWaveletData wavelet, DeltaSequence deltas) { Set<ParticipantId> currentAndNewParticipants = Sets.newHashSet(wavelet.getParticipants()); for (TransformedWaveletDelta delta : deltas) { // Participants added or removed in this delta get the whole delta. for (WaveletOperation op : delta) { if (op instanceof AddParticipant) { ParticipantId p = ((AddParticipant) op).getParticipantId(); currentAndNewParticipants.add(p); } } } // Robot should receive also deltas that contain AddParticipant ops. // EventGenerator will take care to filter out events before the add. for (ParticipantId participant : currentAndNewParticipants) { RobotName robotName = RobotName.fromAddress(participant.getAddress()); if (robotName == null) { // Not a valid robot name, next. continue; } ParticipantId robotId = ParticipantId.ofUnsafe(robotName.toEmailAddress()); AccountData account; try { account = accountStore.getAccount(robotId); } catch (PersistenceException e) { LOG.severe("Failed to retrieve the account data for " + robotId.getAddress(), e); continue; } if (account != null && account.isRobot()) { RobotAccountData robotAccount = account.asRobot(); if (robotAccount.isVerified()) { Robot robot = getOrCreateRobot(robotName, robotAccount); updateRobot(robot, wavelet, deltas); } } } } /** * Gets or creates a {@link Robot} for the given name. * * @param robotName the name of the robot. * @param account the {@link RobotAccountData} belonging to the given * {@link RobotName}. */ private Robot getOrCreateRobot(RobotName robotName, RobotAccountData account) { Robot robot = allRobots.get(robotName); if (robot == null) { robot = createNewRobot(robotName, account); allRobots.put(robotName, robot); } return robot; } /** * Creates a new {@link Robot}. * * @param robotName the name of the robot. * @param account the {@link RobotAccountData} belonging to the given * {@link RobotName}. */ private Robot createNewRobot(RobotName robotName, RobotAccountData account) { EventGenerator eventGenerator = new EventGenerator(robotName, conversationUtil); RobotOperationApplicator operationApplicator = new RobotOperationApplicator(converterManager, waveletProvider, new OperationServiceRegistryImpl(notifyOpService), conversationUtil); return new Robot(robotName, account, this, connector, converterManager, waveletProvider, eventGenerator, operationApplicator); } /** * Updates a {@link Robot} with information about a waveletUpdate event. * * @param robot The robot to process the update for. * @param wavelet the wavelet on which the update is occuring. * @param deltas the deltas the have been applied to the given wavelet. */ private void updateRobot(Robot robot, ReadableWaveletData wavelet, DeltaSequence deltas) { try { robot.waveletUpdate(wavelet, deltas); ensureScheduled(robot); } catch (OperationException e) { LOG.warning("Unable to update robot(" + robot.getRobotName() + ")", e); } } /** * Ensures that a robot is submitted to the executor, might submit the * {@link Robot} if it hasn't been submitted yet. * * <p> * Synchronized in combination with done() to keep proper track of the robots * that have been submitted. * * @param robot the {@link Robot} to enqueue */ public synchronized void ensureScheduled(Robot robot) { if (!runnableRobots.contains(robot.getRobotName())) { LOG.info("Enqueing robot: " + robot.getRobotName()); runnableRobots.add(robot.getRobotName()); executor.execute(robot); } } /** * Signal that a robot is done running. Synchronized with ensureRunnable since * that method needs to have a synchronized view on the runnableRobots for * submitting task to the executor. * * @param robot the {@link Robot} which is done working. */ public synchronized void doneRunning(Robot robot) { runnableRobots.remove(robot.getRobotName()); } /** * Updates the account for the given {@link Robot}. * * @param robot the {@link Robot} to update. * @throws CapabilityFetchException if the capabilities could not be fetched * or parsed. */ public void updateRobotAccount(Robot robot) throws CapabilityFetchException, PersistenceException { // TODO: Pass in activeAPIUrl String activeApiUrl = ""; RobotAccountData newAccount = connector.fetchCapabilities(robot.getAccount(), activeApiUrl); accountStore.putAccount(newAccount); robot.setAccount(newAccount); } }