com.google.wave.api.AbstractRobot.java Source code

Java tutorial

Introduction

Here is the source code for com.google.wave.api.AbstractRobot.java

Source

/* Copyright (c) 2009 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;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.wave.api.JsonRpcConstant.ParamsProperty;
import com.google.wave.api.OperationRequest.Parameter;
import com.google.wave.api.event.AnnotatedTextChangedEvent;
import com.google.wave.api.event.BlipContributorsChangedEvent;
import com.google.wave.api.event.BlipSubmittedEvent;
import com.google.wave.api.event.DocumentChangedEvent;
import com.google.wave.api.event.Event;
import com.google.wave.api.event.EventHandler;
import com.google.wave.api.event.EventType;
import com.google.wave.api.event.FormButtonClickedEvent;
import com.google.wave.api.event.GadgetStateChangedEvent;
import com.google.wave.api.event.OperationErrorEvent;
import com.google.wave.api.event.WaveletBlipCreatedEvent;
import com.google.wave.api.event.WaveletBlipRemovedEvent;
import com.google.wave.api.event.WaveletCreatedEvent;
import com.google.wave.api.event.WaveletFetchedEvent;
import com.google.wave.api.event.WaveletParticipantsChangedEvent;
import com.google.wave.api.event.WaveletSelfAddedEvent;
import com.google.wave.api.event.WaveletSelfRemovedEvent;
import com.google.wave.api.event.WaveletTitleChangedEvent;
import com.google.wave.api.impl.EventMessageBundle;
import com.google.wave.api.impl.GsonFactory;
import com.google.wave.api.impl.WaveletData;

import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.SimpleOAuthValidator;
import net.oauth.signature.OAuthSignatureMethod;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.logging.Logger;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * A robot is an automated participant on a wave, that can read the contents
 * of a wave in which it participates, modify the wave's contents, add or remove
 * participants, and create new blips and new waves. In short, a robot can
 * perform many of the actions that any other participant can perform.
 *
 * This is the abstract base class for a Google Wave Java robot, that supports:
 * <ul>
 *   <li>Automatic events deserialization and operations serialization,
 *       in the event based model</li>
 *   <li>OAuth-secured operations submission, in the active model</li>
 *   <li>Callback for profile request, including proxied/custom profile</li>
 *   <li>Callback for capabilities.xml support</li>
 *   <li>Callback for verification token request, that is used during the robot
 *       registration process, to obtain consumer key and secret</li>
 * </ul>
 *
 * Robot should implements the handlers of the events that it's interested in,
 * and specify the context and filter (if applicable) via the
 * {@link EventHandler.Capability} annotation. For example, if it is interested
 * in a {@link BlipSubmittedEvent}, and would like to get the parent blip with
 * the incoming event bundle, then it should implement this method:
 * <pre>
 *   @Capability(contexts = {Context.PARENT, Context.SELF})
 *   public void onBlipSubmitted(BlipSubmittedEvent e) {
 *     ...
 *   }
 * </pre>
 * If the robot does not specify the {@link EventHandler.Capability}
 * annotation, the default contexts (parent and children), and empty filter will
 * be provided by default.
 */
public abstract class AbstractRobot extends HttpServlet implements EventHandler {

    /**
     * Helper class to make outgoing HTTP request.
     */
    static class HttpFetcher {

        /**
         * Sends a request to the specified URL.
         *
         * @param url the URL to send the request to.
         * @param contentType the content type of the request body.
         * @param body the request body.
         * @return the response from the server.
         *
         * @throws IOException if there is a problem sending the request, or the
         *         HTTP response code is not HTTP OK.
         */
        public String send(String url, String contentType, String body) throws IOException {
            OutputStreamWriter out = null;
            try {
                // Open the connection.
                HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();

                // Send the request body.
                conn.setDoOutput(true);
                conn.setRequestProperty("Content-Type", contentType);
                out = new OutputStreamWriter(conn.getOutputStream());
                out.write(body);
                out.flush();

                // Read the response
                BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                StringBuilder result = new StringBuilder();
                String s;
                while ((s = reader.readLine()) != null) {
                    result.append(s);
                }

                // Throw an exception if the response is not OK.
                if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    LOG.severe("Invalid response: " + result.toString());
                    throw new IOException("HTTP Response code is not OK: " + conn.getResponseCode());
                }

                return result.toString();
            } finally {
                if (out != null) {
                    out.close();
                }
            }
        }
    }

    /**
     * Helper class that contains various OAuth credentials.
     */
    private static class ConsumerData {

        /** Consumer key used to sign the operations in the active mode. */
        private final String consumerKey;

        /** Consumer secret used to sign the operations in the active mode. */
        private final String consumerSecret;

        /** The URL that handles the JSON-RPC request in the active mode. */
        private final String rpcServerUrl;

        /**
         * Constructor.
         *
         * @param consumerKey the consumer key.
         * @param consumerSecret the consumer secret.
         * @param rpcServerUrl the URL of the JSON-RPC request handler.
         */
        public ConsumerData(String consumerKey, String consumerSecret, String rpcServerUrl) {
            this.consumerKey = consumerKey;
            this.consumerSecret = consumerSecret;
            this.rpcServerUrl = rpcServerUrl;
        }

        /**
         * @return the consumer key used to sign the operations in the active mode.
         */
        public String getConsumerKey() {
            return consumerKey;
        }

        /**
         * @return the consumer secret used to sign the operations in the active mode.
         */
        public String getConsumerSecret() {
            return consumerSecret;
        }

        /**
         * @return the URL of the JSON-RPC request handler.
         */
        public String getRpcServerUrl() {
            return rpcServerUrl;
        }
    }

    /** Some mime types. */
    public static final String JSON_MIME_TYPE = "application/json; charset=utf-8";
    public static final String TEXT_MIME_TYPE = "text/plain";
    public static final String XML_MIME_TYPE = "application/xml";

    /** Some constants for encoding. */
    public static final String UTF_8 = "utf-8";
    public static final String SHA_1 = "SHA-1";
    public static final String OAUTH_BODY_HASH = "oauth_body_hash";
    public static final String OAUTH_CONSUMER_KEY_DOMAIN = "google.com";

    public static final String POST = "POST";

    /** The query parameter to specify custom profile request. */
    public static final String NAME_QUERY_PARAMETER_KEY = "name";

    /** The query parameter for security token. */
    public static final String SECURITY_TOKEN_PARAMETER_KEY = "st";

    /** Various request path constants that the robot replies to. */
    public static final String RPC_PATH = "/_wave/robot/jsonrpc";
    public static final String PROFILE_PATH = "/_wave/robot/profile";
    public static final String CAPABILITIES_PATH = "/_wave/capabilities.xml";
    public static final String VERIFY_TOKEN_PATH = "/_wave/verify_token";
    public static final String DEFAULT_AVATAR = "https://wave.google.com/a/wavesandbox.com/static/images/profiles/rusty.png";

    private static final Logger LOG = Logger.getLogger(AbstractRobot.class.getName());
    private static final String PROTOCOL_VERSION = "0.2";
    private static final String ACTIVE_API_OPERATION_NAMESPACE = "wave";

    /** Serializer to serialize events and operations in the event-based mode. */
    private static final Gson SERIALIZER = new GsonFactory().create();

    /** Serializer to serialize events and operations in active mode. */
    private static final Gson SERIALIZER_FOR_ACTIVE_API = new GsonFactory().create(ACTIVE_API_OPERATION_NAMESPACE);

    /** A utility to make HTTP requests. */
    private final HttpFetcher httpFetcher;

    /** A map of this robot's capabilities. */
    private final Map<String, Capability> capabilityMap;

    /** A version number that is computed from this robot's capabilities. */
    private final String version;

    /** A map of RPC server URL to its consumer data object. */
    private final Map<String, ConsumerData> consumerData = new HashMap<String, ConsumerData>();

    /** The incoming HTTP request, only set in the event-based mode. */
    private HttpServletRequest request;

    /** The token used to verify author during the registration process. */
    private String verificationToken;

    /** The token that is checked when handling verification token request. */
    private String securityToken;

    private boolean allowUnsignedRequests = true;

    /**
     * Constructor.
     */
    protected AbstractRobot() {
        this(new HttpFetcher());
    }

    /**
     * Constructor for testing.
     *
     * @param httpFetcher a utility class to make outgoing HTTP requests. Specify
     *     a mock fetcher for unit tests.
     */
    AbstractRobot(HttpFetcher httpFetcher) {
        this.httpFetcher = httpFetcher;
        this.capabilityMap = computeCapabilityMap();
        this.version = computeHash();
    }

    /**
     * Submits the pending operations associated with this {@link Wavelet}.
     *
     * @param wavelet the bundle that contains the operations to be submitted.
     * @param rpcServerUrl the active gateway to send the operations to.
     * @return a list of {@link JsonRpcResponse} that represents the responses
     *     from the server for all operations that were submitted.
     *
     * @throws IllegalStateException if this method is called prior to setting
     *     the proper consumer key, secret, and handler URL.
     * @throws IOException if there is a problem submitting the operations.
     */
    public List<JsonRpcResponse> submit(Wavelet wavelet, String rpcServerUrl) throws IOException {
        OperationQueue opQueue = wavelet.getOperationQueue();
        List<JsonRpcResponse> responses = makeRpc(opQueue, rpcServerUrl);
        wavelet.getOperationQueue().clear();
        return responses;
    }

    /**
     * Returns an empty/blind stub of a wavelet with the given wave id and wavelet
     * id.
     *
     * Call this method if you would like to apply wavelet-only operations
     * without fetching the wave first.
     *
     * The returned wavelet has its own {@link OperationQueue}. It is the
     * responsibility of the caller to make sure this wavelet gets submitted to
     * the server, either by calling {@link AbstractRobot#submit(Wavelet, String)}
     * or by calling {@link Wavelet#submitWith(Wavelet)} on the new wavelet, to
     * join its queue with another wavelet, for example, the event wavelet.
     *
     * @param waveId the id of the wave.
     * @param waveletId the id of the wavelet.
     * @return a stub of a wavelet.
     */
    public Wavelet blindWavelet(WaveId waveId, WaveletId waveletId) {
        return blindWavelet(waveId, waveletId, null);
    }

    /**
     * Returns an empty/blind stub of a wavelet with the given wave id and wavelet
     * id.
     *
     * Call this method if you would like to apply wavelet-only operations
     * without fetching the wave first.
     *
     * The returned wavelet has its own {@link OperationQueue}. It is the
     * responsibility of the caller to make sure this wavelet gets submitted to
     * the server, either by calling {@link AbstractRobot#submit(Wavelet, String)}
     * or by calling {@link Wavelet#submitWith(Wavelet)} on the new wavelet, to
     * join its queue with another wavelet, for example, the event wavelet.
     *
     * @param waveId the id of the wave.
     * @param waveletId the id of the wavelet.
     * @param proxyForId the proxying information that should be set on the
     *     operation queue.
     * @return a stub of a wavelet.
     */
    public Wavelet blindWavelet(WaveId waveId, WaveletId waveletId, String proxyForId) {
        return blindWavelet(waveId, waveletId, proxyForId, new HashMap<String, Blip>());
    }

    /**
     * Returns an empty/blind stub of a wavelet with the given wave id and wavelet
     * id.
     *
     * Call this method if you would like to apply wavelet-only operations
     * without fetching the wave first.
     *
     * The returned wavelet has its own {@link OperationQueue}. It is the
     * responsibility of the caller to make sure this wavelet gets submitted to
     * the server, either by calling {@link AbstractRobot#submit(Wavelet, String)}
     * or by calling {@link Wavelet#submitWith(Wavelet)} on the new wavelet, to
     * join its queue with another wavelet, for example, the event wavelet.
     *
     * @param waveId the id of the wave.
     * @param waveletId the id of the wavelet.
     * @param proxyForId the proxying information that should be set on the
     *     operation queue.
     * @param blips a collection of blips that belong to this wavelet.
     * @return a stub of a wavelet.
     */
    public Wavelet blindWavelet(WaveId waveId, WaveletId waveletId, String proxyForId, Map<String, Blip> blips) {
        return new Wavelet(waveId, waveletId, null, Collections.<String>emptySet(), blips,
                new OperationQueue(proxyForId));
    }

    /**
     * Creates a new wave with a list of participants on it.
     *
     * The root wavelet of the new wave is returned with its own
     * {@link OperationQueue}. It is the responsibility of the caller to make sure
     * this wavelet gets submitted to the server, either by calling
     * {@link AbstractRobot#submit(Wavelet, String)} or by calling
     * {@link Wavelet#submitWith(Wavelet)} on the new wavelet.
     *
     * @param domain the domain to create the wavelet on. In general, this should
     *     correspond to the domain of the incoming event wavelet, except when
     *     the robot is calling this method outside of an event or when the server
     *     is handling multiple domains.
     * @param participants the initial participants on the wave. The robot, as the
     *     creator of the wave, will be added by default. The order of the
     *     participants will be preserved.
     */
    public Wavelet newWave(String domain, Set<String> participants) {
        return newWave(domain, participants, null);
    }

    /**
     * Creates a new wave with a list of participants on it.
     *
     * The root wavelet of the new wave is returned with its own
     * {@link OperationQueue}. It is the responsibility of the caller to make sure
     * this wavelet gets submitted to the server, either by calling
     * {@link AbstractRobot#submit(Wavelet, String)} or by calling
     * {@link Wavelet#submitWith(Wavelet)} on the new wavelet.
     *
     * @param domain the domain to create the wavelet on. In general, this should
     *     correspond to the domain of the incoming event wavelet, except when
     *     the robot is calling this method outside of an event or when the server
     *     is handling multiple domains.
     * @param participants the initial participants on the wave. The robot, as the
     *     creator of the wave, will be added by default. The order of the
     *     participants will be preserved.
     * @param proxyForId the proxy id that should be used to create the new wave.
     *     If specified, the creator of the wave would be
     *     robotid+<proxyForId>@appspot.com.
     */
    public Wavelet newWave(String domain, Set<String> participants, String proxyForId) {
        return newWave(domain, participants, "", proxyForId);
    }

    /**
     * Creates a new wave with a list of participants on it.
     *
     * The root wavelet of the new wave is returned with its own
     * {@link OperationQueue}. It is the responsibility of the caller to make sure
     * this wavelet gets submitted to the server, either by calling
     * {@link AbstractRobot#submit(Wavelet, String)} or by calling
     * {@link Wavelet#submitWith(Wavelet)} on the new wavelet.
     *
     * @param domain the domain to create the wavelet on. In general, this should
     *     correspond to the domain of the incoming event wavelet, except when
     *     the robot is calling this method outside of an event or when the server
     *     is handling multiple domains.
     * @param participants the initial participants on the wave. The robot, as the
     *     creator of the wave, will be added by default. The order of the
     *     participants will be preserved.
     * @param msg the message that will be passed back to the robot when
     *     WAVELET_CREATED event is fired as a result of this operation.
     * @param proxyForId the proxy id that should be used to create the new wave.
     *     If specified, the creator of the wave would be
     *     robotid+<proxyForId>@appspot.com.
     */
    public Wavelet newWave(String domain, Set<String> participants, String msg, String proxyForId) {
        return new OperationQueue(proxyForId).createWavelet(domain, participants, msg);
    }

    /**
     * Creates a new wave with a list of participants on it.
     *
     * The root wavelet of the new wave is returned with its own
     * {@link OperationQueue}. It is the responsibility of the caller to make sure
     * this wavelet gets submitted to the server, either by calling
     * {@link AbstractRobot#submit(Wavelet, String)} or by calling
     * {@link Wavelet#submitWith(Wavelet)} on the new wavelet.
     *
     * @param domain the domain to create the wavelet on. In general, this should
     *     correspond to the domain of the incoming event wavelet, except when
     *     the robot is calling this method outside of an event or when the server
     *     is handling multiple domains.
     * @param participants the initial participants on the wave. The robot, as the
     *     creator of the wave, will be added by default. The order of the
     *     participants will be preserved.
     * @param msg the message that will be passed back to the robot when
     *     WAVELET_CREATED event is fired as a result of this operation.
     * @param proxyForId the proxy id that should be used to create the new wave.
     *     If specified, the creator of the wave would be
     *     robotid+<proxyForId>@appspot.com.
     * @param rpcServerUrl if specified, this operation will be submitted
     *     immediately to this active gateway, that will return immediately the
     *     actual wave id, the id of the root wavelet, and id of the root blip.
     *
     * @throws IOException if there is a problem submitting the operation to the
     *      server, when {@code submit} is {@code true}.
     */
    public Wavelet newWave(String domain, Set<String> participants, String msg, String proxyForId,
            String rpcServerUrl) throws IOException {
        OperationQueue opQueue = new OperationQueue(proxyForId);
        Wavelet newWavelet = opQueue.createWavelet(domain, participants, msg);

        if (rpcServerUrl != null && !rpcServerUrl.isEmpty()) {
            JsonRpcResponse response = this.submit(newWavelet, rpcServerUrl).get(0);
            if (response.isError()) {
                throw new IOException(response.getErrorMessage());
            }
            WaveId waveId = WaveId.deserialise((String) response.getData().get(ParamsProperty.WAVE_ID));
            WaveletId waveletId = WaveletId.deserialise((String) response.getData().get(ParamsProperty.WAVELET_ID));
            String rootBlipId = (String) response.getData().get(ParamsProperty.BLIP_ID);

            Map<String, Blip> blips = new HashMap<String, Blip>();
            newWavelet = new Wavelet(waveId, waveletId, rootBlipId, participants, blips, opQueue);
            blips.put(rootBlipId, new Blip(rootBlipId, "", null, newWavelet));
        }
        return newWavelet;
    }

    /**
     * Fetches a wavelet using the active API.
     *
     * The returned wavelet contains a snapshot of the state of the wavelet at
     * that point. It can be used to modify the wavelet, but the wavelet might
     * change in between, so treat carefully.
     *
     * Also, the returned wavelet has its own {@link OperationQueue}. It is the
     * responsibility of the caller to make sure this wavelet gets submitted to
     * the server, either by calling {@link AbstractRobot#submit(Wavelet, String)}
     * or by calling {@link Wavelet#submitWith(Wavelet)} on the new wavelet.
     *
     * @param waveId the id of the wave to fetch.
     * @param waveletId the id of the wavelet to fetch.
     * @param rpcServerUrl the active gateway that is used to fetch the wavelet.
     *
     * @throws IOException if there is a problem fetching the wavelet.
     */
    public Wavelet fetchWavelet(WaveId waveId, WaveletId waveletId, String rpcServerUrl) throws IOException {
        return fetchWavelet(waveId, waveletId, null, rpcServerUrl);
    }

    /**
     * Fetches a wavelet using the active API.
     *
     * The returned wavelet contains a snapshot of the state of the wavelet at
     * that point. It can be used to modify the wavelet, but the wavelet might
     * change in between, so treat carefully.
     *
     * Also, the returned wavelet has its own {@link OperationQueue}. It is the
     * responsibility of the caller to make sure this wavelet gets submitted to
     * the server, either by calling {@link AbstractRobot#submit(Wavelet, String)}
     * or by calling {@link Wavelet#submitWith(Wavelet)} on the new wavelet.
     *
     * @param waveId the id of the wave to fetch.
     * @param waveletId the id of the wavelet to fetch.
     * @param proxyForId the proxy id that should be used to fetch this wavelet.
     * @param rpcServerUrl the active gateway that is used to fetch the wavelet.
     *
     * @throws IOException if there is a problem fetching the wavelet.
     */
    public Wavelet fetchWavelet(WaveId waveId, WaveletId waveletId, String proxyForId, String rpcServerUrl)
            throws IOException {
        OperationQueue opQueue = new OperationQueue(proxyForId);
        opQueue.fetchWavelet(waveId, waveletId);

        JsonRpcResponse response = makeRpc(opQueue, rpcServerUrl).get(0);
        if (response.isError()) {
            throw new IOException(response.getErrorMessage());
        }

        // Deserialize wavelet.
        opQueue.clear();
        WaveletData waveletData = (WaveletData) response.getData().get(ParamsProperty.WAVELET_DATA);
        Map<String, Blip> blips = new HashMap<String, Blip>();
        Wavelet wavelet = Wavelet.deserialize(opQueue, blips, waveletData);

        // Deserialize blips.
        @SuppressWarnings("unchecked")
        Map<String, BlipData> blipDatas = (Map<String, BlipData>) response.getData().get(ParamsProperty.BLIPS);
        for (Entry<String, BlipData> entry : blipDatas.entrySet()) {
            blips.put(entry.getKey(), Blip.deserialize(opQueue, wavelet, entry.getValue()));
        }

        return wavelet;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        this.request = req;
        if (req.getRequestURI().equals(RPC_PATH)) {
            processRpc(req, resp);
        } else {
            resp.setStatus(HttpURLConnection.HTTP_NOT_FOUND);
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.request = req;
        String path = req.getRequestURI();
        if (path.equals(PROFILE_PATH)) {
            processProfile(req, resp);
        } else if (path.equals(CAPABILITIES_PATH)) {
            processCapabilities(req, resp);
        } else if (path.equals(VERIFY_TOKEN_PATH)) {
            processVerifyToken(req, resp);
        } else {
            resp.setStatus(HttpURLConnection.HTTP_NOT_FOUND);
        }
    }

    /**
     * @return a custom profile based on "name" query parameter, or {@code null}
     *     if this robot doesn't support custom profile.
     */
    protected ParticipantProfile getCustomProfile(@SuppressWarnings("unused") String name) {
        return null;
    }

    /**
     * @return the URL of the robot's avatar image.
     */
    protected String getRobotAvatarUrl() {
        return DEFAULT_AVATAR;
    }

    /**
     * @return the URL of the robot's profile page.
     */
    protected String getRobotProfilePageUrl() {
        return "http://" + request.getRemoteHost();
    }

    /**
     * Sets up the verification token that is used for the owner verification
     * step during the robot registration.
     *
     * @param verificationToken the verification token.
     * @param securityToken the security token that should be matched against when
     *     serving a verification token request.
     */
    protected void setupVerificationToken(String verificationToken, String securityToken) {
        this.verificationToken = verificationToken;
        this.securityToken = securityToken;
    }

    /**
     * Sets the OAuth related properties, including the consumer key and secret
     * that are used to sign the outgoing operations in the active mode. Robot
     * developer needs to follow the registration process to obtain the key and
     * secret:
     * <ul>
     *   <li>http://wave.google.com/wave/robot/register - for wave preview</li>
     *   <li>https://wave.google.com/a/wavesandbox.com/robot/register - for
     *       wave sandbox</li>
     * </ul>
     * <br/>
     * You can call this method multiple times to set up both preview and wave
     * sandbox.
     * <br/>
     * After calling this method, the robot no longer accepts unsigned requests,
     * but if you still want it, you can set
     * {@link #setAllowUnsignedRequests(boolean)} to {@code true}.
     * @param consumerKey the consumer key.
     * @param consumerSecret the consumer secret.
     * @param rpcServerUrl the URL of the server that serves the JSON-RPC request.
     *     <ul>
     *       <li>http://www-opensocial.googleusercontent.com/api/rpc - for wave
     *           preview.<li>
     *       <li>http://www-opensocial-sandbox.googleusercontent.com/api/rpc - for
     *           wave sandbox.</li>
     *     </ul>
     *
     * @throws IllegalArgumentException if any of the arguments are {@code null}.
     */
    protected void setupOAuth(String consumerKey, String consumerSecret, String rpcServerUrl) {
        if (consumerKey == null || consumerSecret == null || rpcServerUrl == null) {
            throw new IllegalArgumentException(
                    "Consumer Key, Consumer Secret and RPCServerURL " + "has to be non-null");
        }
        ConsumerData consumerDataObj = new ConsumerData(consumerKey, consumerSecret, rpcServerUrl);
        this.consumerData.put(rpcServerUrl, consumerDataObj);
        setAllowUnsignedRequests(false);
    }

    /**
     * Sets the OAuth related properties, including the consumer key and secret
     * that are used to sign the outgoing operations in the active mode. This
     * method sets the JSON-RPC handler URL to http://gmodules.com/api/rpc,
     * that is associated with wave preview instance.
     *
     * @param consumerKey the consumer key.
     * @param consumerSecret the consumer secret.
     */
    protected void setupOAuth(String consumerKey, String consumerSecret) {
        setupOAuth(consumerKey, consumerSecret, "http://www-opensocial.googleusercontent.com/api/rpc");
    }

    /**
     * Sets whether or not unsigned incoming requests from robot proxy are
     * allowed.
     *
     * @param allowUnsignedRequests whether or not unsigned requests from robot
     *     proxy are allowed.
     */
    protected void setAllowUnsignedRequests(boolean allowUnsignedRequests) {
        if (!allowUnsignedRequests && this.consumerData.isEmpty()) {
            throw new IllegalArgumentException("Please call AbstractRobot.setupOAuth() first to "
                    + "setup the consumer key and secret to validate the request.");
        }
        this.allowUnsignedRequests = allowUnsignedRequests;
    }

    /**
     * @return {@code true} if unsigned incoming requests from robot proxy are
     *     allowed.
     */
    protected boolean isUnsignedRequestsAllowed() {
        return allowUnsignedRequests;
    }

    /**
     * Processes the incoming HTTP request to obtain the verification token.
     *
     * @param req the HTTP request.
     * @param resp the HTTP response.
     */
    private void processVerifyToken(HttpServletRequest req, HttpServletResponse resp) {
        if (verificationToken == null || verificationToken.isEmpty()) {
            LOG.info("Please register a verification token by calling " + "AbstractRobot.setVerificationToken().");
            resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
            return;
        }

        String incomingSecurityToken = req.getParameter(SECURITY_TOKEN_PARAMETER_KEY);
        if (securityToken != null && !securityToken.equals(incomingSecurityToken)) {
            LOG.info("The incoming security token " + incomingSecurityToken + " does not match the "
                    + "expected security token " + securityToken + ".");
            resp.setStatus(HttpURLConnection.HTTP_UNAUTHORIZED);
            return;
        }

        resp.setContentType(TEXT_MIME_TYPE);
        resp.setCharacterEncoding(UTF_8);
        try {
            resp.getWriter().write(verificationToken);
        } catch (IOException e) {
            resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
            return;
        }
        resp.setStatus(HttpURLConnection.HTTP_OK);
    }

    /**
     * Processes the incoming HTTP request to obtain the capabilities.xml file.
     *
     * @param req the HTTP request.
     * @param resp the HTTP response.
     */
    private void processCapabilities(HttpServletRequest req, HttpServletResponse resp) {
        StringBuilder xml = new StringBuilder();
        xml.append("<?xml version=\"1.0\"?>\n");
        xml.append("<w:robot xmlns:w=\"http://wave.google.com/extensions/robots/1.0\">\n");
        xml.append("  <w:version>");
        xml.append(version);
        xml.append("</w:version>\n");
        xml.append("  <w:protocolversion>");
        xml.append(PROTOCOL_VERSION);
        xml.append("</w:protocolversion>\n");
        xml.append("  <w:capabilities>\n");
        for (Entry<String, Capability> entry : capabilityMap.entrySet()) {
            xml.append("    <w:capability name=\"" + entry.getKey() + "\"");
            Capability capability = entry.getValue();
            if (capability != null) {
                // Append context.
                if (capability.contexts().length != 0) {
                    xml.append(" context=\"");
                    boolean first = true;
                    for (Context context : capability.contexts()) {
                        if (first) {
                            first = false;
                        } else {
                            xml.append(',');
                        }
                        xml.append(context.name());
                    }
                    xml.append("\"");
                }

                // Append filter.
                if (capability.filter() != null && !capability.filter().isEmpty()) {
                    xml.append(" filter=\"");
                    xml.append(capability.filter());
                    xml.append("\"");
                }
            }
            xml.append("/>\n");
        }
        xml.append("  </w:capabilities>\n");
        if (!consumerData.keySet().isEmpty()) {
            xml.append("  <w:consumer_keys>\n");
            for (ConsumerData consumerDataObj : consumerData.values()) {
                xml.append("    <w:consumer_key for=\"" + consumerDataObj.getRpcServerUrl() + "\">"
                        + consumerDataObj.getConsumerKey() + "</w:consumer_key>\n");
            }
            xml.append("  </w:consumer_keys>\n");
        }
        xml.append("</w:robot>\n");
        // Write the result into the output stream.
        resp.setContentType(XML_MIME_TYPE);
        resp.setCharacterEncoding(UTF_8);
        try {
            resp.getWriter().write(xml.toString());
        } catch (IOException e) {
            resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
            return;
        }
        resp.setStatus(HttpURLConnection.HTTP_OK);
    }

    /**
     * Processes the incoming HTTP request to obtain robot's profile.
     *
     * @param req the HTTP request.
     * @param resp the HTTP response.
     */
    private void processProfile(HttpServletRequest req, HttpServletResponse resp) {
        ParticipantProfile profile = null;

        // Try to get custom profile.
        String proxiedName = req.getParameter(NAME_QUERY_PARAMETER_KEY);
        if (proxiedName != null) {
            profile = getCustomProfile(proxiedName);
        }

        // Set the default profile.
        if (profile == null) {
            profile = new ParticipantProfile(getRobotName(), getRobotAvatarUrl(), getRobotProfilePageUrl());
        }

        // Write the result into the output stream.
        resp.setContentType(JSON_MIME_TYPE);
        resp.setCharacterEncoding(UTF_8);
        try {
            resp.getWriter().write(SERIALIZER.toJson(profile));
        } catch (IOException e) {
            resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
            return;
        }
        resp.setStatus(HttpURLConnection.HTTP_OK);
    }

    /**
     * Processes the incoming HTTP request that contains the event bundle.
     *
     * @param req the HTTP request.
     * @param resp the HTTP response.
     */
    private void processRpc(HttpServletRequest req, HttpServletResponse resp) {
        // Deserialize and process the incoming events.
        EventMessageBundle events = null;
        try {
            events = deserializeEvents(req);
        } catch (IOException e) {
            resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
            return;
        }

        // Append robot.notifyCapabilitiesHash operation before processing the
        // events.
        OperationQueue operationQueue = events.getWavelet().getOperationQueue();
        operationQueue.appendOperation(OperationType.ROBOT_NOTIFY_CAPABILITIES_HASH,
                Parameter.of(ParamsProperty.CAPABILITIES_HASH, version));

        // Call the robot event handlers.
        processEvents(events);

        // Serialize the operations.
        serializeOperations(operationQueue.getPendingOperations(), resp);
        operationQueue.clear();
    }

    /**
     * Processes the incoming event bundle. This method iterates over the event
     * bundle and dispatch the individual event to its own handler, based on the
     * event type.
     *
     * @param events the incoming event bundle.
     */
    protected void processEvents(EventMessageBundle events) {
        for (Event event : events.getEvents()) {
            switch (event.getType()) {
            case ANNOTATED_TEXT_CHANGED:
                onAnnotatedTextChanged(AnnotatedTextChangedEvent.as(event));
                break;
            case BLIP_CONTRIBUTORS_CHANGED:
                onBlipContributorsChanged(BlipContributorsChangedEvent.as(event));
                break;
            case BLIP_SUBMITTED:
                onBlipSubmitted(BlipSubmittedEvent.as(event));
                break;
            case DOCUMENT_CHANGED:
                onDocumentChanged(DocumentChangedEvent.as(event));
                break;
            case FORM_BUTTON_CLICKED:
                onFormButtonClicked(FormButtonClickedEvent.as(event));
                break;
            case GADGET_STATE_CHANGED:
                onGadgetStateChanged(GadgetStateChangedEvent.as(event));
                break;
            case WAVELET_BLIP_CREATED:
                onWaveletBlipCreated(WaveletBlipCreatedEvent.as(event));
                break;
            case WAVELET_BLIP_REMOVED:
                onWaveletBlipRemoved(WaveletBlipRemovedEvent.as(event));
                break;
            case WAVELET_CREATED:
                onWaveletCreated(WaveletCreatedEvent.as(event));
                break;
            case WAVELET_FETCHED:
                onWaveletFetched(WaveletFetchedEvent.as(event));
                break;
            case WAVELET_PARTICIPANTS_CHANGED:
                onWaveletParticipantsChanged(WaveletParticipantsChangedEvent.as(event));
                break;
            case WAVELET_SELF_ADDED:
                onWaveletSelfAdded(WaveletSelfAddedEvent.as(event));
                break;
            case WAVELET_SELF_REMOVED:
                onWaveletSelfRemoved(WaveletSelfRemovedEvent.as(event));
                break;
            case WAVELET_TITLE_CHANGED:
                onWaveletTitleChanged(WaveletTitleChangedEvent.as(event));
                break;
            case OPERATION_ERROR:
                onOperationError(OperationErrorEvent.as(event));
                break;
            }
        }
    }

    /**
     * Computes this robot's capabilities, based on the overriden event handler
     * methods, and their {@link EventHandler.Capability} annotations.
     *
     * The result map does not use {@link EventType} enum as the key for stability
     * between JVM runs, since the same enum may have different hashcode between
     * JVM runs. This may cause two instances of the same robot that are running
     * on different JVMs (for example, when App Engine scale the robot) to have
     * different version number and capabilities ordering in
     * {@code capabilities.xml}.
     *
     * @return a map of event type string to capability.
     */
    protected Map<String, Capability> computeCapabilityMap() {
        Map<String, Capability> map = new HashMap<String, Capability>();
        for (Method baseMethod : EventHandler.class.getDeclaredMethods()) {
            Method overridenMethod = null;
            try {
                overridenMethod = this.getClass().getDeclaredMethod(baseMethod.getName(),
                        baseMethod.getParameterTypes());
            } catch (NoSuchMethodException e) {
                // Robot does not override this particular event handler. Continue.
                continue;
            }

            // Get the event type.
            EventType eventType = EventType.fromClass(overridenMethod.getParameterTypes()[0]);

            // Get the capability annotation.
            Capability capability = overridenMethod.getAnnotation(Capability.class);

            map.put(eventType.toString(), capability);
        }
        return map;
    }

    /**
     * Computes this robot's hash, based on the capabilities.
     *
     * @return a hash of this robot, computed from it's capabilities.
     */
    private String computeHash() {
        long version = 0l;
        for (Entry<String, Capability> entry : capabilityMap.entrySet()) {
            long hash = entry.getKey().hashCode();
            Capability capability = entry.getValue();
            if (capability != null) {
                for (Context context : capability.contexts()) {
                    hash = hash * 31 + context.name().hashCode();
                }
                hash = hash * 31 + capability.filter().hashCode();
            }
            version = version * 17 + hash;
        }
        return Long.toHexString(version);
    }

    /**
     * Deserializes the given HTTP request's JSON body into an event message
     * bundle.
     *
     * @param req the HTTP request to be deserialized.
     * @return an event message bundle.
     *
     * @throws IOException if there is a problem reading the request's body.
     * @throws IllegalArgumentException if the request is not signed properly.
     */
    private EventMessageBundle deserializeEvents(HttpServletRequest req) throws IOException {
        String json = readRequestBody(req);
        LOG.info("Incoming events: " + json);

        EventMessageBundle bundle = SERIALIZER.fromJson(json, EventMessageBundle.class);

        if (bundle.getRpcServerUrl() == null) {
            throw new IllegalArgumentException("RPC server URL is not set in the event bundle.");
        }

        // Get the OAuth credentials for the given RPC server URL.
        ConsumerData consumerDataObj = consumerData.get(bundle.getRpcServerUrl());
        if (consumerDataObj == null && !isUnsignedRequestsAllowed()) {
            throw new IllegalArgumentException(
                    "No consumer key is found for the RPC server URL: " + bundle.getRpcServerUrl());
        }

        // Validates the request.
        if (consumerDataObj != null) {
            try {
                @SuppressWarnings("unchecked")
                Map<String, String[]> parameterMap = request.getParameterMap();
                validateOAuthRequest(request.getRequestURL().toString(), parameterMap, json,
                        consumerDataObj.getConsumerKey(), consumerDataObj.getConsumerSecret());
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalArgumentException("Error validating OAuth request", e);
            } catch (URISyntaxException e) {
                throw new IllegalArgumentException("Error validating OAuth request", e);
            } catch (OAuthException e) {
                throw new IllegalArgumentException("Error validating OAuth request", e);
            }
        }
        return bundle;
    }

    /**
     * Submits the given operations.
     *
     * @param opQueue the operation queue to be submitted.
     * @param rpcServerUrl the active gateway to send the operations to.
     * @return a list of {@link JsonRpcResponse} that represents the responses
     *     from the server for all operations that were submitted.
     *
     * @throws IllegalStateException if this method is called prior to setting
     *     the proper consumer key, secret, and handler URL.
     * @throws IOException if there is a problem submitting the operations.
     */
    private List<JsonRpcResponse> makeRpc(OperationQueue opQueue, String rpcServerUrl) throws IOException {
        if (rpcServerUrl == null) {
            throw new IllegalStateException("RPC Server URL is not set up.");
        }

        ConsumerData consumerDataObj = consumerData.get(rpcServerUrl);
        if (consumerDataObj == null) {
            throw new IllegalStateException("Consumer key, consumer secret, and  JSON-RPC server URL "
                    + "have to be set first, by calling AbstractRobot.setupOAuth(), before invoking "
                    + "AbstractRobot.submit().");
        }

        String json = SERIALIZER_FOR_ACTIVE_API.toJson(opQueue.getPendingOperations(),
                new TypeToken<List<OperationRequest>>() {
                }.getType());

        try {
            String url = createOAuthUrlString(json, consumerDataObj.getRpcServerUrl(),
                    consumerDataObj.getConsumerKey(), consumerDataObj.getConsumerSecret());
            LOG.info("JSON request to be sent: " + json);

            String responseString = httpFetcher.send(url, JSON_MIME_TYPE, json);

            LOG.info("Response returned: " + responseString);

            List<JsonRpcResponse> responses = null;
            if (responseString.startsWith("[")) {
                Type listType = new TypeToken<List<JsonRpcResponse>>() {
                }.getType();
                responses = SERIALIZER_FOR_ACTIVE_API.fromJson(responseString, listType);
            } else {
                responses = new ArrayList<JsonRpcResponse>(1);
                responses.add(SERIALIZER_FOR_ACTIVE_API.fromJson(responseString, JsonRpcResponse.class));
            }
            return responses;
        } catch (OAuthException e) {
            LOG.warning("OAuthException when constructing the OAuth parameters: " + e);
            throw new IOException(e);
        } catch (URISyntaxException e) {
            LOG.warning("URISyntaxException when constructing the OAuth parameters: " + e);
            throw new IOException(e);
        }
    }

    /**
     * Serializes the given outgoing operations into a JSON string, and put it in
     * the given response object.
     *
     * @param operations the operations to be serialized.
     * @param resp the response object to flush the output string into.
     */
    private static void serializeOperations(List<OperationRequest> operations, HttpServletResponse resp) {
        try {
            String json = SERIALIZER.toJson(operations, new TypeToken<List<OperationRequest>>() {
            }.getType());
            LOG.info("Outgoing operations: " + json);

            resp.setContentType(JSON_MIME_TYPE);
            resp.setCharacterEncoding(UTF_8);
            resp.getWriter().write(json);
            resp.setStatus(HttpURLConnection.HTTP_OK);
        } catch (IOException iox) {
            resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
        }
    }

    /**
     * Reads the given HTTP request's input stream into a string.
     *
     * @param req the HTTP request to be read.
     * @return a string representation of the given HTTP request's body.
     *
     * @throws IOException if there is a problem reading the body.
     */
    private static String readRequestBody(HttpServletRequest req) throws IOException {
        StringBuilder json = new StringBuilder();
        BufferedReader reader = req.getReader();
        String line;
        while ((line = reader.readLine()) != null) {
            json.append(line);
        }
        return json.toString();
    }

    /**
     * Creates a URL that contains the necessary OAuth query parameters for the
     * given JSON string.
     *
     * @param jsonBody the JSON string to construct the URL from.
     * @param rpcServerUrl the URL of the handler that services the JSON-RPC
     *     request.
     * @param consumerKey the OAuth consumerKey.
     * @param consumerSecret the OAuth consumerSecret.
     *
     * @return a URL for the given JSON string, and the required OAuth parameters:
     *     <ul>
     *       <li>oauth_body_hash</li>
     *       <li>oauth_consumer_key</li>
     *       <li>oauth_signature_method</li>
     *       <li>oauth_timestamp</li>
     *       <li>oauth_nonce</li>
     *       <li>oauth_version</li>
     *       <li>oauth_signature</li>
     *     </ul>
     */
    private static String createOAuthUrlString(String jsonBody, String rpcServerUrl, String consumerKey,
            String consumerSecret) throws IOException, URISyntaxException, OAuthException {
        OAuthMessage message = new OAuthMessage(POST, rpcServerUrl, Collections.<Entry<String, String>>emptyList());

        // Compute the hash of the body.
        byte[] hash = DigestUtils.sha(jsonBody);
        byte[] encodedHash = Base64.encodeBase64(hash, false);
        message.addParameter(OAUTH_BODY_HASH, new String(encodedHash, UTF_8));

        // Add other parameters.
        OAuthConsumer consumer = new OAuthConsumer(null, OAUTH_CONSUMER_KEY_DOMAIN + ":" + consumerKey,
                consumerSecret, null);
        consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
        OAuthAccessor accessor = new OAuthAccessor(consumer);
        message.addRequiredParameters(accessor);
        LOG.info("Signature base string: " + OAuthSignatureMethod.getBaseString(message));

        // Construct the resulting URL.
        StringBuilder sb = new StringBuilder(rpcServerUrl);
        char connector = '?';
        for (Map.Entry<String, String> p : message.getParameters()) {
            if (!p.getKey().equals(jsonBody)) {
                sb.append(connector);
                sb.append(URLEncoder.encode(p.getKey(), "UTF-8"));
                sb.append('=');
                sb.append(URLEncoder.encode(p.getValue(), "UTF-8"));
                connector = '&';
            }
        }
        return sb.toString();
    }

    /**
     * Validates the incoming HTTP request.
     *
     * @param requestUrl the URL of the request.
     * @param jsonBody the request body to be validated.
     * @param consumerKey the consumer key.
     * @param consumerSecret the consumer secret.
     */
    private static void validateOAuthRequest(String requestUrl, Map<String, String[]> requestParams,
            String jsonBody, String consumerKey, String consumerSecret)
            throws NoSuchAlgorithmException, IOException, URISyntaxException, OAuthException {
        List<OAuth.Parameter> params = new ArrayList<OAuth.Parameter>();
        for (Entry<String, String[]> entry : requestParams.entrySet()) {
            for (String value : entry.getValue()) {
                params.add(new OAuth.Parameter(entry.getKey(), value));
            }
        }
        OAuthMessage message = new OAuthMessage(POST, requestUrl, params);

        // Compute and check the hash of the body.
        MessageDigest md = MessageDigest.getInstance(SHA_1);
        byte[] hash = md.digest(jsonBody.getBytes(UTF_8));
        String encodedHash = new String(Base64.encodeBase64(hash, false), UTF_8);
        if (!encodedHash.equals(message.getParameter(OAUTH_BODY_HASH))) {
            throw new IllegalArgumentException("Body hash does not match. Expected: " + encodedHash + ", provided: "
                    + message.getParameter(OAUTH_BODY_HASH));
        }

        // Construct validator arguments.
        OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, null);
        OAuthAccessor accessor = new OAuthAccessor(consumer);
        LOG.info("Signature base string: " + OAuthSignatureMethod.getBaseString(message));
        message.validateMessage(accessor, new SimpleOAuthValidator());
    }

    @Override
    public void onAnnotatedTextChanged(AnnotatedTextChangedEvent event) {
        // No-op.
    }

    @Override
    public void onBlipContributorsChanged(BlipContributorsChangedEvent event) {
        // No-op.
    }

    @Override
    public void onBlipSubmitted(BlipSubmittedEvent event) {
        // No-op.
    }

    @Override
    public void onDocumentChanged(DocumentChangedEvent event) {
        // No-op.
    }

    @Override
    public void onFormButtonClicked(FormButtonClickedEvent event) {
        // No-op.
    }

    @Override
    public void onGadgetStateChanged(GadgetStateChangedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletBlipCreated(WaveletBlipCreatedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletBlipRemoved(WaveletBlipRemovedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletCreated(WaveletCreatedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletFetched(WaveletFetchedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletParticipantsChanged(WaveletParticipantsChangedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletSelfAdded(WaveletSelfAddedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletSelfRemoved(WaveletSelfRemovedEvent event) {
        // No-op.
    }

    @Override
    public void onWaveletTitleChanged(WaveletTitleChangedEvent event) {
        // No-op.
    }

    @Override
    public void onOperationError(OperationErrorEvent event) {
        // No-op.
    }

    /**
     * @return the display name of the robot.
     */
    protected abstract String getRobotName();
}