/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.RawAttachmentData;
import com.google.wave.api.impl.GsonFactory;
import com.google.wave.api.impl.RawDeltasListener;
import com.google.wave.api.impl.WaveletData;
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 org.waveprotocol.wave.media.model.AttachmentId;
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 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.Level;
import java.util.logging.Logger;
/**
* Utility class for using OAuth to talk to Wave service.
*/
public class WaveService {
/** Infinite {@code urlfetch} fetch timeout. */
public static final int FETCH_INFINITE_TIMEOUT = 0;
/** Default {@code urlfetch} fetch timeout in ms. */
public static final int FETCH_DEFAILT_TIMEOUT_IN_MS = 10 * 1000;
/**
* Helper class to make outgoing OAuth HTTP requests.
*/
static class HttpFetcher implements HttpClient {
private static final String HTTP_POST_METHOD = "POST";
private static final String HTTP_PUT_METHOD = "PUT";
private int fetchTimeout = FETCH_DEFAILT_TIMEOUT_IN_MS;
@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(fetchTimeout);
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();
}
/**
* Sets the fetch timeout to a specified timeout, in milliseconds.
* A timeout of zero is interpreted as an infinite timeout.
*
* @param fetchTimeout
*/
void setTimeout(int fetchTimeout) {
this.fetchTimeout = fetchTimeout;
}
}
/**
* 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();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("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);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
return (SearchResult) response.get(ParamsProperty.SEARCH_RESULTS);
}
/**
* 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);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
// Deserialize wavelet.
opQueue.clear();
WaveletData waveletData = (WaveletData) response.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.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.get(ParamsProperty.BLIPS);
for (Map.Entry<String, BlipData> entry : blipDatas.entrySet()) {
blips.put(entry.getKey(), Blip.deserialize(opQueue, wavelet, entry.getValue()));
}
return wavelet;
}
/**
* Retrieves wavelets ids of the specified wave.
*
* @param waveId the id of the wave.
* @param rpcServerUrl the URL of the JSON-RPC request handler.
* @return list of wavelets ids.
* @throws IOException if there is a problem fetching the wavelet.
*/
public List<WaveletId> retrieveWaveletIds(WaveId waveId, String rpcServerUrl)
throws IOException {
OperationQueue opQueue = new OperationQueue();
opQueue.retrieveWaveletIds(waveId);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
@SuppressWarnings("unchecked")
List<WaveletId> list = (List<WaveletId>)response.get(ParamsProperty.WAVELET_IDS);
return list;
}
/**
* Exports wavelet deltas history.
*
* @param waveId the id of the wave to export.
* @param waveletId the id of the wavelet to export.
* @param rpcServerUrl the URL of the JSON-RPC request handler.
* @return WaveletSnapshot in Json.
* @throws IOException if there is a problem fetching the wavelet.
*/
public String exportRawSnapshot(WaveId waveId, WaveletId waveletId, String rpcServerUrl) throws IOException {
OperationQueue opQueue = new OperationQueue();
opQueue.exportSnapshot(waveId, waveletId);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
return (String)response.get(ParamsProperty.RAW_SNAPSHOT);
}
/**
* Exports wavelet deltas history.
*
* @param waveId the id of the wave to export.
* @param waveletId the id of the wavelet to export.
* @param fromVersion start ProtocolHashedVersion.
* @param toVersion end ProtocolHashedVersion.
* @param rpcServerUrl the URL of the JSON-RPC request handler.
* @return history of deltas.
* @throws IOException if there is a problem fetching the deltas.
*/
public void exportRawDeltas(WaveId waveId, WaveletId waveletId,
byte[] fromVersion, byte[] toVersion, String rpcServerUrl,
RawDeltasListener listener) throws IOException {
OperationQueue opQueue = new OperationQueue();
opQueue.exportRawDeltas(waveId, waveletId, fromVersion, toVersion);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
@SuppressWarnings("unchecked")
List<byte[]> rawHistory = (List<byte[]>)response.get(ParamsProperty.RAW_DELTAS);
byte[] rawTargetVersion = (byte[])response.get(ParamsProperty.TARGET_VERSION);
listener.onSuccess(rawHistory, rawTargetVersion);
}
/**
* Exports attachment.
*
* @param attachmentId the id of attachment.
* @param rpcServerUrl the URL of the JSON-RPC request handler.
* @return the data of attachment.
* @throws IOException if there is a problem fetching the wavelet.
*/
public RawAttachmentData exportAttachment(AttachmentId attachmentId,
String rpcServerUrl) throws IOException {
OperationQueue opQueue = new OperationQueue();
opQueue.exportAttachment(attachmentId);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
return (RawAttachmentData)response.get(ParamsProperty.ATTACHMENT_DATA);
}
/**
* Imports deltas to wavelet.
*
* @param waveId the id of the wave to import.
* @param waveletId the id of the wavelet to import.
* @param history the history of deltas.
* @param rpcServerUrl the URL of the JSON-RPC request handler.
* @return the version from which importing started.
* @throws IOException if there is a problem fetching the wavelet.
*/
public long importRawDeltas(WaveId waveId, WaveletId waveletId,
List<byte[]> history, String rpcServerUrl) throws IOException {
OperationQueue opQueue = new OperationQueue();
opQueue.importRawDeltas(waveId, waveletId, history);
Map<ParamsProperty, Object> response = makeSingleOperationRpc(opQueue, rpcServerUrl);
return (Long)response.get(ParamsProperty.IMPORTED_FROM_VERSION);
}
/**
* Imports attachment.
*
* @param waveId the id of the wave to import.
* @param waveletId the id of the wavelet to import.
* @param attachmentId the id of attachment.
* @param attachmentData the data of attachment.
* @param rpcServerUrl the URL of the JSON-RPC request handler.
* @throws IOException if there is a problem fetching the wavelet.
*/
public void importAttachment(WaveId waveId, WaveletId waveletId, AttachmentId attachmentId,
RawAttachmentData attachmentData, String rpcServerUrl) throws IOException {
OperationQueue opQueue = new OperationQueue();
opQueue.importAttachment(waveId, waveletId, attachmentId, attachmentData);
makeSingleOperationRpc(opQueue, rpcServerUrl);
}
/**
* @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 operation.
*
* @param opQueue the operation queue with operation to be submitted.
* @param rpcServerUrl the active gateway to send the operations to.
* @return the data of response.
* @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, or error response.
*/
private Map<ParamsProperty, Object> makeSingleOperationRpc(OperationQueue opQueue, String rpcServerUrl)
throws IOException {
JsonRpcResponse response = makeRpc(opQueue, rpcServerUrl).get(0);
if (response.isError()) {
throw new IOException(response.getErrorMessage());
}
return response.getData();
}
/**
* 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());
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("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, JSON_MIME_TYPE));
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);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("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));
}
responses.remove(0); // removes response to the notify operation.
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);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("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();
}
/**
* Sets the fetch timeout to a specified timeout, in milliseconds.
* A timeout of zero is interpreted as an infinite timeout.
*
* @param fetchTimeout
*/
public void setFetchFimeout(int fetchTimeout) {
httpFetcher.setTimeout(fetchTimeout);
}
}