Java tutorial
/* * 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 waveimport; 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.Proto.ProtocolAppliedWaveletDelta; import com.google.walkaround.proto.RobotSearchDigest; import com.google.walkaround.proto.gson.RobotSearchDigestGsonImpl; 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.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(@Assisted 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 WIAB_ROBOT_API_CREATE_WAVELET = "robot.createWavelet"; 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) { return true; } if (!EXPECTED_CONTENT_TYPE.equals(fetch.getSingleHeader(resp, "Content-Type"))) { return false; } JSONObject result = parseJsonResponseBody(resp); try { if (result.has("error")) { JSONObject error = result.getJSONObject("error"); return error.has("code") && error.getInt("code") == 401; } 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 = fetch.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(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); } 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)); System.out.println("req: " + ops.toString()); JSONObject result = parseJsonResponseBody(fetch.fetch(req, robotErrorCode401Detector)); 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 IOException("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); } } private JSONObject callRobotApi1(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); } 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)); System.out.println("req: " + ops.toString()); JSONObject result = parseJsonResponseBody(fetch.fetch(req, robotErrorCode401Detector)); log.info("result=" + ValueUtils.abbrev("" + result, 500)); return result; } 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 wave. */ 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 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); } } /** * Fetch wave with deltas * @author A. Kaplanov */ public JSONObject fetchWaveWithDeltas(WaveId waveId, WaveletId waveletId) throws IOException { return callRobotApi1(ROBOT_API_METHOD_FETCH_WAVE, getFetchWaveParamMap(WaveletName.of(waveId, waveletId), "rawDeltasFromVersion", 0)); } /** * 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)); System.out.println("gson :" + response.toString()); // 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(); } public void wiabImportWave(Object waveletData) throws IOException { JSONObject response = callRobotApi(WIAB_ROBOT_API_CREATE_WAVELET, ImmutableMap.<String, Object>of("waveletData", waveletData)); } }