/* Copyright (c) 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 com.google.wave.api;

import com.google.gson.Gson;
import com.google.wave.api.JsonRpcConstant.ParamsProperty;
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.OAuthValidator;
import net.oauth.SimpleOAuthValidator;
import net.oauth.client.OAuthClient;
import net.oauth.http.HttpClient;
import net.oauth.http.HttpMessage;
import net.oauth.http.HttpResponseMessage;
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.InvalidIdException;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
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.AbstractMap.SimpleEntry;
import java.util.logging.Logger;

/**
 * Utility class for using OAuth to talk to Wave service.
 */
public class WaveService {

  /**
   * Helper class to make outgoing OAuth HTTP requests.
   */
  static class HttpFetcher implements HttpClient {

    /** The {@code urlfetch} fetch timeout in ms. */
    private static final int URLFETCH_TIMEOUT_IN_MS = 10 * 1000;

    private static final String HTTP_POST_METHOD = "POST";
    private static final String HTTP_PUT_METHOD = "PUT";

    @Override
    public HttpResponseMessage execute(HttpMessage request, Map<String, Object> stringObjectMap)
        throws IOException {
      String body = readInputStream(request.getBody());
      OutputStreamWriter out = null;
      HttpURLConnection conn = null;
      // Open the connection.
      conn = (HttpURLConnection) request.url.openConnection();
      conn.setReadTimeout(URLFETCH_TIMEOUT_IN_MS);
      conn.setRequestMethod(request.method);
      // Add the headers
      if (request.headers != null) {
        for (java.util.Map.Entry<String, String> header : request.headers) {
          conn.setRequestProperty(header.getKey(), header.getValue());
        }
      }

      boolean doOutput =
          body != null && (HTTP_POST_METHOD.equalsIgnoreCase(request.method)
              || HTTP_PUT_METHOD.equalsIgnoreCase(request.method));

      if (doOutput) {
        conn.setDoOutput(true);
      }

      conn.connect();

      if (doOutput) {
        // Send the request body.
        out = new OutputStreamWriter(conn.getOutputStream(), UTF_8);
        try {
          out.write(body);
          out.flush();
        } finally {
          out.close();
        }
      }

      // Return the response stream.
      return new HttpResponse(
          request.method, request.url, conn.getResponseCode(), conn.getInputStream());
    }

    /**
     * Reads the given {@link java.io.InputStream} into a {@link String}
     *
     * @param inputStream the {@link java.io.InputStream} to be read.
     * @return a string content of the {@link java.io.InputStream}.
     * @throws IOException if there is a problem reading the stream.
     */
    static String readInputStream(InputStream inputStream) throws IOException {
      if (inputStream == null) {
        return null;
      }
      BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
      StringBuilder result = new StringBuilder();
      String s;
      while ((s = reader.readLine()) != null) {
        result.append(s);
      }
      return result.toString();
    }
  }

  /**
   * A simple implementation of {@link HttpResponseMessage} that gets the
   * response from {@link HttpURLConnection#getInputStream()}.
   */
  static class HttpResponse extends HttpResponseMessage {

    /** The HTTP response code. */
    private final int statusCode;

    /** The response stream. */
    private final InputStream responseStream;

    /**
     * Constructor.
     *
     * @param method the HTTP method, for example, GET or POST.
     * @param url the URL where the response comes from.
     * @param statusCode the HTTP response code.
     * @param responseStream the response stream.
     */
    public HttpResponse(String method, URL url, int statusCode, InputStream responseStream) {
      super(method, url);
      this.statusCode = statusCode;
      this.responseStream = responseStream;
    }

    @Override
    public int getStatusCode() {
      return statusCode;
    }

    @Override
    public InputStream openBody() {
      return responseStream;
    }
  }

  /**
   * Helper class that contains various OAuth credentials.
   */
  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;

    /** Whether this session is user authenticated */
    private final boolean userAuthenticated;

    /** The OAuth Accessor contains authentication data used to make requests */
    private final OAuthAccessor accessor;

    /**
     * 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) {
      String consumerKeyPrefix = "";
      // NOTE(ljvderijk): Present for backwards capability.
      if (RPC_URL.equals(rpcServerUrl) || SANDBOX_RPC_URL.equals(rpcServerUrl)) {
        consumerKeyPrefix = "google.com:";
      }
      this.consumerKey = consumerKeyPrefix + consumerKey;
      this.consumerSecret = consumerSecret;
      this.rpcServerUrl = rpcServerUrl;

      userAuthenticated = false;
      OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, null);
      consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
      accessor = new OAuthAccessor(consumer);
    }

    public ConsumerData(OAuthAccessor accessor, String rpcServerUrl) {
      this.consumerKey = accessor.consumer.consumerKey;
      this.consumerSecret = accessor.consumer.consumerSecret;
      this.accessor = accessor;
      this.rpcServerUrl = rpcServerUrl;
      userAuthenticated = true;
    }

    /**
     * @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;
    }

    public boolean isUserAuthenticated() {
      return userAuthenticated;
    }

    public OAuthAccessor getAccessor() {
      return accessor;
    }

  }

  /** The wire protocol version. */
  public static final ProtocolVersion PROTOCOL_VERSION = ProtocolVersion.DEFAULT;

  private static final String JSON_MIME_TYPE = "application/json; charset=utf-8";
  private static final String OAUTH_BODY_HASH = "oauth_body_hash";
  private static final String POST = "POST";
  private static final String SHA_1 = "SHA-1";
  private static final String UTF_8 = "UTF-8";

  /** Wave RPC URLs. */
  public static final String RPC_URL = "https://www-opensocial.googleusercontent.com/api/rpc";
  public static final String SANDBOX_RPC_URL =
      "https://www-opensocial-sandbox.googleusercontent.com/api/rpc";

  private static final Logger LOG = Logger.getLogger(WaveService.class.getName());

  /** Namespace to prefix all active api operation calls. */
  private static final String OPERATION_NAMESPACE = "wave";

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

  /** OAuth request validator. */
  private static final OAuthValidator VALIDATOR = new SimpleOAuthValidator();

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

  /** A version number. */
  private final String version;

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

  /**
   * Constructor.
   */
  public WaveService() {
    this(new HttpFetcher(), null);
  }

  /**
   * Constructor.
   *
   * @param version the version number.
   */
  public WaveService(String version) {
    this(new HttpFetcher(), version);
  }

  /**
   * Constructor.
   *
   * @param httpFetcher the fetcher to make HTTP calls.
   * @param version the version number.
   */
  public WaveService(HttpFetcher httpFetcher, String version) {
    this.httpFetcher = httpFetcher;
    this.version = version;
  }

  /**
   * Sets the OAuth related properties, including the consumer key and secret
   * that are used to sign the outgoing operations.
   *
   * <p>
   * This version of the method is for 2-legged OAuth, where the robot is not
   * acting on behalf of a user.
   *
   * <p>
   * For the rpcServerUrl you can use:
   * <ul>
   * <li>https://www-opensocial.googleusercontent.com/api/rpc - for wave
   * preview.
   * <li>
   * https://www-opensocial-sandbox.googleusercontent.com/api/rpc - for wave
   * sandbox.
   * </ul>
   *
   * @param consumerKey the consumer key.
   * @param consumerSecret the consumer secret.
   * @param rpcServerUrl the URL of the server that serves the JSON-RPC request.
   */
  public void setupOAuth(String consumerKey, String consumerSecret, String rpcServerUrl) {
    if (consumerKey == null || consumerSecret == null || rpcServerUrl == null) {
      throw new IllegalArgumentException(
          "Consumer Key, Consumer Secret and RPCServerURL " + "have to be non-null");
    }
    consumerDataMap.put(rpcServerUrl, new ConsumerData(consumerKey, consumerSecret, rpcServerUrl));
  }

  /**
   * Sets the OAuth related properties that are used to sign the outgoing
   * operations for 3-legged OAuth.
   *
   * <p>
   * Performing the OAuth dance is not part of this interface - once you've done
   * the dance, pass the constructed accessor and rpc endpoint into this method.
   *
   * <p>
   * Ensure that the endpoint URL you pass in matches exactly the URL used to
   * request an access token (including https vs http).
   *
   *  For the rpcServerUrl you can use:
   * <ul>
   * <li>https://www-opensocial.googleusercontent.com/api/rpc - for wave
   * preview.
   * <li>
   * https://www-opensocial-sandbox.googleusercontent.com/api/rpc - for wave
   * sandbox.
   * </ul>
   *
   * @param accessor the {@code OAuthAccessor} with access token and secret
   * @param rpcServerUrl the endpoint URL of the server that serves the JSON-RPC
   *        request.
   */
  public void setupOAuth(OAuthAccessor accessor, String rpcServerUrl) {
    if (accessor == null || rpcServerUrl == null) {
      throw new IllegalArgumentException("Accessor and RPCServerURL have to be non-null");
    }
    consumerDataMap.put(rpcServerUrl, new ConsumerData(accessor, rpcServerUrl));
  }

  /**
   * Validates the incoming HTTP request.
   *
   * @param requestUrl the URL of the request.
   * @param jsonBody the request body to be validated.
   * @param rpcServerUrl the RPC server URL.
   *
   * @throws OAuthException if it can't validate the request.
   */
  public void validateOAuthRequest(
      String requestUrl, Map<String, String[]> requestParams, String jsonBody, String rpcServerUrl)
      throws OAuthException {
    ConsumerData consumerData = consumerDataMap.get(rpcServerUrl);
    if (consumerData == null) {
      throw new IllegalArgumentException(
          "There is no consumer key and secret associated " + "with the given RPC URL "
              + rpcServerUrl);
    }

    List<OAuth.Parameter> params = new ArrayList<OAuth.Parameter>();
    for (Map.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.
    try {
      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));
      }

      OAuthAccessor accessor = consumerData.getAccessor();
      LOG.info("Signature base string: " + OAuthSignatureMethod.getBaseString(message));
      VALIDATOR.validateMessage(message, accessor);
    } catch (NoSuchAlgorithmException e) {
      throw new OAuthException("Error validating OAuth request", e);
    } catch (URISyntaxException e) {
      throw new OAuthException("Error validating OAuth request", e);
    } catch (OAuthException e) {
      throw new OAuthException("Error validating OAuth request", e);
    } catch (IOException e) {
      throw new OAuthException("Error validating OAuth request", e);
    }
  }

  /**
   * 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 {
    List<JsonRpcResponse> responses = makeRpc(wavelet.getOperationQueue(), rpcServerUrl);
    wavelet.getOperationQueue().clear();
    return responses;
  }

  /**
   * Returns an empty/blind stub of a wavelet with the given wave id and wavelet
   * id.
   *
   * <p>
   * 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);
  }

  /**
   * @see #blindWavelet(WaveId, WaveletId)
   *
   * @param proxyForId the proxying information that should be set on the
   *        operation queue. Please note that this parameter should be properly
   *        encoded to ensure that the resulting participant id is valid (see
   *        {@link Util#checkIsValidProxyForId(String)} for more details).
   */
  public Wavelet blindWavelet(WaveId waveId, WaveletId waveletId, String proxyForId) {
    return blindWavelet(waveId, waveletId, proxyForId, new HashMap<String, Blip>());
  }

  /**
   * @see #blindWavelet(WaveId, WaveletId, String)
   *
   * @param blips a collection of blips that belong to this wavelet.
   */
  public Wavelet blindWavelet(
      WaveId waveId, WaveletId waveletId, String proxyForId, Map<String, Blip> blips) {
    return blindWavelet(waveId, waveletId, proxyForId, blips, new HashMap<String, BlipThread>());
  }

  /**
   * @see #blindWavelet(WaveId, WaveletId, String, Map)
   *
   * @param threads a collection of threads that belong to this wavelet.
   */
  public Wavelet blindWavelet(WaveId waveId, WaveletId waveletId, String proxyForId,
      Map<String, Blip> blips, Map<String, BlipThread> threads) {
    Util.checkIsValidProxyForId(proxyForId);
    Map<String, String> roles = new HashMap<String, String>();
    return new Wavelet(waveId, waveletId, null,
        new BlipThread("", -1, new ArrayList<String>(), blips), Collections.<String>emptySet(),
        roles, blips, threads, 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);
  }

  /**
   * @see #newWave(String, Set)
   *
   * @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. Please note that this parameter
   *        should be properly encoded to ensure that the resulting participant
   *        id is valid (see {@link Util#checkIsValidProxyForId(String)} for
   *        more details).
   */
  public Wavelet newWave(String domain, Set<String> participants, String proxyForId) {
    return newWave(domain, participants, "", proxyForId);
  }

  /**
   * @see #newWave(String, Set, String)
   *
   * @param msg the message that will be passed back to the robot when
   *        WAVELET_CREATED event is fired as a result of this operation.
   */
  public Wavelet newWave(String domain, Set<String> participants, String msg, String proxyForId) {
    Util.checkIsValidProxyForId(proxyForId);
    return new OperationQueue(proxyForId).createWavelet(domain, participants, msg);
  }

  /**
   * @see #newWave(String, Set, String, String)
   *
   * @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}.
   * @throws InvalidIdException
   */
  public Wavelet newWave(
      String domain, Set<String> participants, String msg, String proxyForId, String rpcServerUrl)
      throws IOException, InvalidIdException {
    Util.checkIsValidProxyForId(proxyForId);
    OperationQueue opQueue = new OperationQueue(proxyForId);
    Wavelet newWavelet = opQueue.createWavelet(domain, participants, msg);

    if (rpcServerUrl != null && !rpcServerUrl.isEmpty()) {
      // Get the response for the robot.fetchWavelet() operation, which is the
      // second operation, since makeRpc prepends the robot.notify() operation.
      JsonRpcResponse response = this.submit(newWavelet, rpcServerUrl).get(1);
      if (response.isError()) {
        throw new IOException(response.getErrorMessage());
      }
      WaveId waveId = ApiIdSerializer.instance().deserialiseWaveId(
          (String) response.getData().get(ParamsProperty.WAVE_ID));
      WaveletId waveletId = ApiIdSerializer.instance().deserialiseWaveletId(
          (String) response.getData().get(ParamsProperty.WAVELET_ID));
      String rootBlipId = (String) response.getData().get(ParamsProperty.BLIP_ID);

      Map<String, Blip> blips = new HashMap<String, Blip>();
      Map<String, BlipThread> threads = new HashMap<String, BlipThread>();
      Map<String, String> roles = new HashMap<String, String>();

      List<String> blipIds = new ArrayList<String>();
      blipIds.add(rootBlipId);
      BlipThread rootThread = new BlipThread("", -1, blipIds, blips);

      newWavelet = new Wavelet(waveId, waveletId, rootBlipId, rootThread, participants,
          roles, blips, threads, opQueue);
      blips.put(rootBlipId, new Blip(rootBlipId, "", null, "", newWavelet));
    }
    return newWavelet;
  }
  
  /**
   * Requests SearchResult for a query.
   * 
   * @param query the query to execute.
   * @param index the index from which to return results.
   * @param numresults the number of results to return.
   * @param rpcServerUrl the active gateway.
   * 
   * @throws IOException if remote server returns error.
   */
  public SearchResult search(String query, Integer index, Integer numResults, String rpcServerUrl)
      throws IOException {
    OperationQueue opQueue = new OperationQueue();
    opQueue.search(query, index, numResults);
    JsonRpcResponse response = makeRpc(opQueue, rpcServerUrl).get(1);
    if (response.isError()) {
      throw new IOException(response.getErrorMessage());
    }
    opQueue.clear();
    SearchResult searchResult =
        (SearchResult) response.getData().get(ParamsProperty.SEARCH_RESULTS);
    return searchResult;
  }

  /**
   * 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);
  }

  /**
   * @see #fetchWavelet(WaveId, WaveletId, String)
   *
   * @param proxyForId the proxy id that should be used to fetch this wavelet.
   *        Please note that this parameter should be properly encoded to ensure
   *        that the resulting participant id is valid (see
   *        {@link Util#checkIsValidProxyForId(String)} for more details).
   */
  public Wavelet fetchWavelet(
      WaveId waveId, WaveletId waveletId, String proxyForId, String rpcServerUrl)
      throws IOException {
    Util.checkIsValidProxyForId(proxyForId);
    OperationQueue opQueue = new OperationQueue(proxyForId);
    opQueue.fetchWavelet(waveId, waveletId);

    // Get the response for the robot.fetchWavelet() operation, which is the
    // second operation, since makeRpc prepends the robot.notify() operation.
    JsonRpcResponse response = makeRpc(opQueue, rpcServerUrl).get(1);
    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>();
    Map<String, BlipThread> threads = new HashMap<String, BlipThread>();
    Wavelet wavelet = Wavelet.deserialize(opQueue, blips, threads, waveletData);

    // Deserialize threads.
    @SuppressWarnings("unchecked")
    Map<String, BlipThread> tempThreads =
        (Map<String, BlipThread>) response.getData().get(ParamsProperty.THREADS);
    for (Map.Entry<String, BlipThread> entry : tempThreads.entrySet()) {
      BlipThread thread = entry.getValue();
      threads.put(entry.getKey(),
          new BlipThread(thread.getId(), thread.getLocation(), thread.getBlipIds(), blips));
    }

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

    return wavelet;
  }

  /**
   * @return the map of consumer key and secret.
   */
  protected Map<String, ConsumerData> getConsumerDataMap() {
    return consumerDataMap;
  }

  /**
   * @return {@code true} if this service object contains a consumer key and
   *         secret for the given RPC server URL.
   */
  protected boolean hasConsumerData(String rpcServerUrl) {
    return consumerDataMap.containsKey(rpcServerUrl);
  }

  /**
   * 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 = consumerDataMap.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().");
    }

    opQueue.notifyRobotInformation(PROTOCOL_VERSION, version);
    String json =
        SERIALIZER.toJson(opQueue.getPendingOperations(), GsonFactory.OPERATION_REQUEST_LIST_TYPE);

    try {
      InputStream bodyStream;
      InputStream responseStream;
      try {
        bodyStream = new ByteArrayInputStream(json.getBytes("UTF-8"));
      } catch (UnsupportedEncodingException e) {
        throw new IllegalStateException(e);
      }
      if (!consumerDataObj.isUserAuthenticated()) {
        String url = createOAuthUrlString(
            json, consumerDataObj.getRpcServerUrl(), consumerDataObj.getAccessor());
        LOG.info("JSON request to be sent: " + json);
        HttpMessage request = new HttpMessage("POST", new URL(url), bodyStream);
        request.headers.add(
            new SimpleEntry<String, String>(HttpMessage.CONTENT_TYPE, JSON_MIME_TYPE));
        request.headers.add(new SimpleEntry<String, String>("oauth_version", "1.0"));
        responseStream =
            httpFetcher.execute(request, Collections.<String, Object>emptyMap()).getBody();
      } else {
        OAuthAccessor accessor = consumerDataObj.getAccessor();
        OAuthMessage message = accessor.newRequestMessage("POST", rpcServerUrl, null, bodyStream);
        message.getHeaders().add(
            new SimpleEntry<String, String>(HttpMessage.CONTENT_TYPE, "application/json"));
        message.getHeaders().add(new SimpleEntry<String, String>("oauth_version", "1.0"));
        OAuthClient client = new OAuthClient(httpFetcher);
        responseStream = client.invoke(message, net.oauth.ParameterStyle.BODY).getBodyAsStream();
      }

      String responseString = HttpFetcher.readInputStream(responseStream);
      LOG.info("Response returned: " + responseString);

      List<JsonRpcResponse> responses = null;
      if (responseString.startsWith("[")) {
        responses = SERIALIZER.fromJson(responseString, GsonFactory.JSON_RPC_RESPONSE_LIST_TYPE);
      } else {
        responses = new ArrayList<JsonRpcResponse>(1);
        responses.add(SERIALIZER.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);
    }
  }

  /**
   * Creates a URL that contains the necessary OAuth query parameters for the
   * given JSON string.
   *
   * The required OAuth parameters are:
   * <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>
   *
   * @param jsonBody the JSON string to construct the URL from.
   * @param rpcServerUrl the URL of the handler that services the JSON-RPC
   *        request.
   * @param accessor the OAuth accessor used to create the signed string.
   * @return a URL for the given JSON string, and the required OAuth parameters.
   */
  public static String createOAuthUrlString(
      String jsonBody, String rpcServerUrl, OAuthAccessor accessor)
      throws IOException, URISyntaxException, OAuthException {
    OAuthMessage message =
        new OAuthMessage(POST, rpcServerUrl, Collections.<SimpleEntry<String, String>>emptyList());

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

    // Add other parameters.

    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();
  }
}
