Package com.google.walkaround.wave.server.googleimport

Source Code of com.google.walkaround.wave.server.googleimport.RobotApi

/*
* Copyright 2011 Google Inc. All Rights Reserved.
*
* 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.walkaround.wave.server.googleimport;

import com.google.appengine.api.urlfetch.FetchOptions;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.walkaround.proto.GoogleImport.GoogleDocument;
import com.google.walkaround.proto.GoogleImport.GoogleWavelet;
import com.google.walkaround.proto.RobotSearchDigest;
import com.google.walkaround.proto.gson.RobotSearchDigestGsonImpl;
import com.google.walkaround.util.server.RetryHelper;
import com.google.walkaround.util.server.RetryHelper.PermanentFailure;
import com.google.walkaround.util.server.RetryHelper.RetryableFailure;
import com.google.walkaround.wave.server.auth.OAuthedFetchService;
import com.google.walkaround.wave.server.auth.OAuthedFetchService.TokenRefreshNeededDetector;

import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.waveprotocol.wave.federation.Proto.ProtocolAppliedWaveletDelta;
import org.waveprotocol.wave.model.id.WaveId;
import org.waveprotocol.wave.model.id.WaveletId;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.util.Pair;
import org.waveprotocol.wave.model.util.ValueUtils;

import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
* Simple interface to Google Wave's active robot API.
*
* We don't use the "official" Wave robot API library since it doesn't support
* some of the APIs that we need (raw snapshot/delta export), and implementing
* what we need is easy anyway.  The main difficulty is OAuth, but we already
* have code for that.
*
* @author ohler@google.com (Christian Ohler)
*/
public class RobotApi {

  public interface Factory {
    RobotApi create(String baseUrl);
  }

  @SuppressWarnings("unused")
  private static final Logger log = Logger.getLogger(RobotApi.class.getName());

  private final OAuthedFetchService fetch;
  private final String baseUrl;

  @Inject
  public RobotApi(OAuthedFetchService fetch,
      @Assisted String baseUrl) {
    this.fetch = fetch;
    this.baseUrl = baseUrl;
  }

  private static final String OP_ID = "op_id";

  private static final String ROBOT_API_METHOD_FETCH_WAVE = "wave.robot.fetchWave";
  private static final String ROBOT_API_METHOD_SEARCH = "wave.robot.search";

  private static final String EXPECTED_CONTENT_TYPE = "application/json; charset=UTF-8";

  private final TokenRefreshNeededDetector robotErrorCode401Detector =
      new TokenRefreshNeededDetector() {
        @Override public boolean refreshNeeded(HTTPResponse resp) throws IOException {
          if (resp.getResponseCode() == 401) {
            log.warning("Response code 401: " + resp);
            return true;
          }
          if (!EXPECTED_CONTENT_TYPE.equals(
              OAuthedFetchService.getSingleHeader(resp, "Content-Type"))) {
            return false;
          }
          JSONObject result = parseJsonResponseBody(resp);
          try {
            if (result.has("error")) {
              JSONObject error = result.getJSONObject("error");
              if (error.has("code") && error.getInt("code") == 401) {
                log.warning("Looks like a 401: " + result + ", " + resp);
                return true;
              } else {
                return false;
              }
            } else {
              return false;
            }
          } catch (JSONException e) {
            throw new RuntimeException("JSONException parsing response: " + result, e);
          }
        }
      };

  // Example of the kind of search request body that we send:
  // [{"id":"op_id",
  //   "method":"wave.robot.search",
  //   "params":
  //     {"query":"abc",
  //      "index":0,
  //      "numResults":100
  //     }
  //  }
  // ]
  //
  // Example of the kind of response body that we receive:
  // [{"id":"op_id",
  //   "data":
  //     {"searchResults":
  //       {"query":"abc",
  //        "numResults":1,
  //        "digests":
  //         [{"waveId":"googlewave.com!w+aaaa",
  //           "title":"aaaa",
  //           "participants":
  //             ["aaaa@googlewave.com",
  //              "aaab@googlewave.com",
  //              "aaaa@googlegroups.com"
  //             ],
  //           "lastModified":1111111111111,
  //           "snippet":"aaaa",
  //           "blipCount":2,
  //           "unreadCount":1
  //          }
  //         ]
  //       }
  //     }
  //  }
  // ]

  private JSONObject parseJsonResponseBody(HTTPResponse resp) throws IOException {
    // The response looks like this:
    // [{"id":"op_id", "data":X}]
    // We return the single item in this array.
    String body = OAuthedFetchService.getUtf8ResponseBody(resp, EXPECTED_CONTENT_TYPE);
    try {
      JSONArray items = new JSONArray(body);
      if (items.length() != 1) {
        throw new RuntimeException("Unexpected length: " + items.length() + ": " + items);
      }
      JSONObject item = items.getJSONObject(0);
      if (!OP_ID.equals(item.getString("id"))) {
        throw new RuntimeException("Unexpected id: " + item);
      }
      return item;
    } catch (JSONException e) {
      throw new RuntimeException("JSONException parsing response: " + body, e);
    }
  }

  private JSONObject callRobotApi(final String method, Map<String, Object> params)
      throws IOException {
    JSONArray ops = new JSONArray();
    try {
      JSONObject jsonParams = new JSONObject();
      for (Map.Entry<String, Object> e : params.entrySet()) {
        jsonParams.put(e.getKey(), e.getValue());
      }
      JSONObject op = new JSONObject();
      op.put("params", jsonParams);
      op.put("method", method);
      op.put("id", OP_ID);
      ops.put(op);
    } catch (JSONException e) {
      throw new RuntimeException("Failed to construct JSON object", e);
    }
    final HTTPRequest req = new HTTPRequest(new URL(baseUrl), HTTPMethod.POST,
        FetchOptions.Builder.disallowTruncate().followRedirects()
            .validateCertificate().setDeadline(20.0));
    log.info("payload=" + ops);
    req.setHeader(new HTTPHeader("Content-Type", "application/json; charset=UTF-8"));
    req.setPayload(ops.toString().getBytes(Charsets.UTF_8));
    try {
      return new RetryHelper(RetryHelper.backoffStrategy(0, 1000, 10 * 60 * 1000))
          .run(new RetryHelper.Body<JSONObject>() {
            @Override public JSONObject run() throws RetryableFailure, PermanentFailure {
              JSONObject result;
              try {
                result = parseJsonResponseBody(fetch.fetch(req, robotErrorCode401Detector));
              } catch (IOException e) {
                throw new PermanentFailure("IOException with " + method + ": " + req, e);
              }
              log.info("result=" + ValueUtils.abbrev("" + result, 500));
              try {
                if (result.has("error")) {
                  log.warning("Error result: " + result);
                  JSONObject error = result.getJSONObject("error");
                  throw new RuntimeException("Error from robot API: " + error);
                } else if (result.has("data")) {
                  JSONObject data = result.getJSONObject("data");
                  if (data.length() == 0) {
                    // Apparently, the server often sends {"id":"op_id", "data":{}} when
                    // something went wrong on the server side, so we translate that to an
                    // IOException.
                    throw new RetryableFailure("Robot API response looks like an error: " + result);
                  } else {
                    return data;
                  }
                } else {
                  throw new RuntimeException("Result has neither error nor data: " + result);
                }
              } catch (JSONException e) {
                throw new RuntimeException("JSONException parsing result: " + result, e);
              }
            }
          });
    } catch (PermanentFailure e) {
      throw new IOException("PermanentFailure with " + method + ": " + req, e);
    }
  }

  private Map<String, Object> getFetchWaveParamMap(WaveletName waveletName, Object... extraParams) {
    Preconditions.checkArgument(extraParams.length % 2 == 0,
        "extraParams must come in pairs: %s", extraParams);
    ImmutableMap.Builder<String, Object> b = ImmutableMap.builder();
    b.put("waveId", waveletName.waveId.serialise());
    b.put("waveletId", waveletName.waveletId.serialise());
    for (int i = 0; i < extraParams.length; i += 2) {
      b.put((String) extraParams[i], extraParams[i + 1]);
    }
    return b.build();
  }

  /**
   * Gets the current state of the wavelet.
   */
  public Pair<GoogleWavelet, ImmutableList<GoogleDocument>> getSnapshot(WaveletName waveletName)
      throws IOException {
    JSONObject resp = callRobotApi(ROBOT_API_METHOD_FETCH_WAVE,
        getFetchWaveParamMap(waveletName, "returnRawSnapshot", true));
    try {
      JSONArray snapshot = resp.getJSONArray("rawSnapshot");
      // NOTE(ohler): snapshot array is not of a uniform type: element 0 is the
      // wavelet metadata, the remaining elements are documents.
      GoogleWavelet wavelet = GoogleWavelet.parseFrom(Base64.decodeBase64(snapshot.getString(0)));
      ImmutableList.Builder<GoogleDocument> documents = ImmutableList.builder();
      for (int i = 1; i < snapshot.length(); i++) {
        documents.add(GoogleDocument.parseFrom(Base64.decodeBase64(snapshot.getString(i))));
      }
      return Pair.of(wavelet, documents.build());
    } catch (JSONException e) {
      throw new RuntimeException("Failed to parse snapshot response: " + resp, e);
    }
  }

  /**
   * Gets delta history for the wavelet.  The server limits its response size in
   * various ways but always return at least one delta; this method has the same
   * behavior.
   */
  public List<ProtocolAppliedWaveletDelta> getRawDeltas(WaveletName waveletName, long fromVersion)
      throws IOException {
    JSONObject resp = callRobotApi(ROBOT_API_METHOD_FETCH_WAVE,
        getFetchWaveParamMap(waveletName, "rawDeltasFromVersion", fromVersion));
    try {
      JSONArray deltas = resp.getJSONArray("rawDeltas");
      ImmutableList.Builder<ProtocolAppliedWaveletDelta> out = ImmutableList.builder();
      for (int i = 0; i < deltas.length(); i++) {
        out.add(ProtocolAppliedWaveletDelta.parseFrom(Base64.decodeBase64(deltas.getString(i))));
      }
      return out.build();
    } catch (JSONException e) {
      throw new RuntimeException("Failed to parse deltas response: " + resp, e);
    }
  }

  /**
   * Gets the list of wavelets in a wave that are visible the user.
   */
  public List<WaveletId> getWaveView(WaveId waveId) throws IOException {
    JSONObject resp = callRobotApi(ROBOT_API_METHOD_FETCH_WAVE,
        ImmutableMap.<String, Object>of("waveId", waveId.serialise(), "listWavelets", true));
    try {
      JSONArray ids = resp.getJSONArray("waveletIds");
      ImmutableList.Builder<WaveletId> out = ImmutableList.builder();
      for (int i = 0; i < ids.length(); i++) {
        out.add(WaveletId.deserialise(ids.getString(i)));
      }
      List<WaveletId> view = out.build();
      log.info("getWaveView(" + waveId + ") = " + view);
      return view;
    } catch (JSONException e) {
      throw new RuntimeException("Failed to parse listWavelets response: " + resp, e);
    }
  }

  /**
   * Searches the user's waves.  Returns at most {@code maxResults} results,
   * starting with the {@code startIndex}-th result (0-based index).
   *
   * Note: Google Wave's search feature may limit the overall set of result for
   * any given query to the first N hits (for some N, perhaps 300), regardless
   * of {@code startIndex} and {@code maxResults}, so don't rely on these to
   * iterate over all waves.
   */
  public List<RobotSearchDigest> search(String query, int startIndex, int maxResults)
      throws IOException {
    log.info("search(" + query + ", " + startIndex + ", " + maxResults + ")");
    JSONObject response = callRobotApi(ROBOT_API_METHOD_SEARCH,
        ImmutableMap.<String, Object>of("query", query,
            "index", startIndex,
            "numResults", maxResults));
    // The response looks like this:
    // {"searchResults":
    //   {"query":"after:2008/01/01 before:2010/01/01",
    //    "numResults":1,
    //    "digests":
    //     [{"waveId":"googlewave.com!w+aaaa",
    //       "title":"aaaa",
    //       "participants":
    //         ["aaaa@googlewave.com",
    //          "aaab@googlewave.com",
    //          "aaaa@googlegroups.com"
    //         ],
    //       "lastModified":1111111111111,
    //       "snippet":"aaaa"
    //       "blipCount":2,
    //       "unreadCount":1,
    //      }
    //     ]
    //   }
    // }
    ImmutableList.Builder<RobotSearchDigest> digests = ImmutableList.builder();
    try {
      JSONObject results = response.getJSONObject("searchResults");
      try {
        if (results.getInt("numResults") != results.getJSONArray("digests").length()) {
          throw new RuntimeException("Mismatched numResults and digests array length: "
              + results.getInt("numResults") + " vs. " + results.getJSONArray("digests"));
        }
        JSONArray rawDigests = results.getJSONArray("digests");
        for (int i = 0; i < rawDigests.length(); i++) {
          JSONObject rawDigest = rawDigests.getJSONObject(i);
          try {
            RobotSearchDigest digest = new RobotSearchDigestGsonImpl();
            digest.setWaveId(WaveId.deserialise(rawDigest.getString("waveId")).serialise());
            JSONArray rawParticipants = rawDigest.getJSONArray("participants");
            for (int j = 0; j < rawParticipants.length(); j++) {
              digest.addParticipant(rawParticipants.getString(j));
            }
            digest.setTitle(rawDigest.getString("title"));
            digest.setSnippet(rawDigest.getString("snippet"));
            digest.setLastModifiedMillis(rawDigest.getLong("lastModified"));
            digest.setBlipCount(rawDigest.getInt("blipCount"));
            digest.setUnreadBlipCount(rawDigest.getInt("unreadCount"));
            digests.add(digest);
          } catch (JSONException e) {
            throw new RuntimeException("Failed to parse search digest: " + rawDigest, e);
          }
        }
      } catch (JSONException e) {
        throw new RuntimeException("Failed to parse search results: " + results, e);
      }
    } catch (JSONException e) {
      throw new RuntimeException("Failed to parse search response: " + response, e);
    }
    return digests.build();
  }

}
TOP

Related Classes of com.google.walkaround.wave.server.googleimport.RobotApi

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.