/* Copyright (c) 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.opensocial;
import net.oauth.http.HttpMessage;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.opensocial.auth.AuthScheme;
import org.opensocial.http.HttpClient;
import org.opensocial.http.HttpClientImpl;
import org.opensocial.http.HttpResponseMessage;
import org.opensocial.providers.Provider;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
/**
* OpenSocial RESTful client supporting both the RPC and REST protocols defined
* in the OpenSocial specification as well as two- and three-legged OAuth for
* authentication. This class handles the transmission of requests to
* OpenSocial containers such as orkut and MySpace. Typical usage:
* <pre>
* Client client = new Client(new OrkutProvider(),
new OAuth2LeggedScheme(ORKUT_KEY, ORKUT_SECRET, ORKUT_ID));
Response response = client.send(PeopleService.getViewer());
* </pre>
* The send method either returns a single {@link Response} or a {@link Map} of
* Response objects mapped to ID strings. The data returned from the container
* can be extracted from these objects.
*
* @author Jason Cooper
*/
public class Client {
private Provider provider;
private AuthScheme authScheme;
private HttpClient httpClient;
private static Logger logger = Logger.getLogger("org.opensocial.client");
/**
* Creates and returns a new {@link Client} associated with the passed
* {@link Provider} and {@link AuthScheme}.
*
* @param provider Provider to associate with new Client
* @param authScheme AuthScheme to associate with new Client
*/
public Client(Provider provider, AuthScheme authScheme) {
this(provider, authScheme, new HttpClientImpl());
}
/**
* Creates and returns a new {@link Client} associated with the passed
* {@link Provider} and {@link AuthScheme}.
*
* @param provider Provider to associate with new Client
* @param authScheme AuthScheme to associate with new Client
* @param httpClient HttpClient to use with new Client
*/
public Client(Provider provider, AuthScheme authScheme, HttpClient
httpClient) {
this.provider = provider;
this.authScheme = authScheme;
this.httpClient = httpClient;
}
/**
* Returns the associated {@link Provider}.
*/
public Provider getProvider() {
return provider;
}
/**
* Returns the associated {@link AuthScheme}.
*/
public AuthScheme getAuthScheme() {
return authScheme;
}
/**
* Submits the passed {@link Request} to the associated {@link Provider} and
* returns the container's response data as a {@link Response} object.
*
* @param request Request object (typically returned from static methods in
* service classes) encapsulating all request data including
* endpoint, HTTP method, and any required parameters
* @return Response object encapsulating the response data returned
* by the container
*
* @throws RequestException if the passed request cannot be serialized, the
* container returns an error code, or the response
* cannot be parsed
* @throws IOException if an I/O error prevents a connection from being
* opened or otherwise causes request transmission
* to fail
*/
public Response send(Request request) throws RequestException, IOException {
final String KEY = "key";
Map<String, Request> requests = new HashMap<String, Request>();
requests.put(KEY, request);
Map<String, Response> responses = send(requests);
return responses.get(KEY);
}
/**
* Submits the passed {@link Map} of {@link Request}s to the associated
* {@link Provider} and returns the container's response data as a Map of
* {@link Response} objects mapped to the same IDs as the passed requests. If
* the associated provider supports the OpenSocial RPC protocol, only one
* HTTP request is sent; otherwise, one HTTP request is executed per
* container request.
*
* @param requests Map of Request objects (typically returned from static
* methods in service classes) to ID strings; each object
* encapsulates the data for a single container request
* such as fetching the viewer or creating an activity.
* @return Map of Response objects, each encapsulating the response
* data returned by the container for a single request, to
* the associated ID strings in the passed Map of Request
* objects
*
* @throws RequestException if the passed request cannot be serialized, the
* container returns an error code, or the response
* cannot be parsed
* @throws IOException if an I/O error prevents a connection from being
* opened or otherwise causes request transmission
* to fail
*/
public Map<String, Response> send(Map<String, Request> requests) throws
RequestException, IOException {
if (requests.size() == 0) {
throw new RequestException("Request queue is empty");
}
Map<String, Response> responses = new HashMap<String, Response>();
if (provider.getRpcEndpoint() != null) {
responses = submitRpc(requests);
} else if (provider.getRestEndpoint() != null) {
for (Map.Entry<String, Request> entry : requests.entrySet()) {
Request request = entry.getValue();
provider.preRequest(request);
Response response = submitRestRequest(request);
responses.put(entry.getKey(), response);
provider.postRequest(request, response);
}
} else {
throw new RequestException("Provider has no REST or RPC endpoint set");
}
return responses;
}
private Map<String, Response> submitRpc(Map<String, Request> requests) throws
RequestException, IOException {
Map<String, String> requestHeaders = new HashMap<String, String>();
if (requests.size() == 1 &&
requests.values().iterator().next().getContentType() != null) {
requestHeaders.put(HttpMessage.CONTENT_TYPE,
requests.values().iterator().next().getContentType());
} else {
requestHeaders.put(HttpMessage.CONTENT_TYPE, provider.getContentType());
}
HttpMessage message = authScheme.getHttpMessage(provider, "POST",
buildRpcUrl(requests), requestHeaders, buildRpcPayload(requests));
HttpResponseMessage responseMessage = httpClient.execute(message);
logger.finest(buildLogRecord(requests, responseMessage));
Map<String, Response> responses = Response.parseRpcResponse(requests,
responseMessage, provider.getVersion());
return responses;
}
private Response submitRestRequest(Request request) throws RequestException,
IOException{
Map<String, String> requestHeaders = new HashMap<String, String>();
if (request.getContentType() != null) {
requestHeaders.put(HttpMessage.CONTENT_TYPE, request.getContentType());
} else {
requestHeaders.put(HttpMessage.CONTENT_TYPE, provider.getContentType());
}
HttpMessage message = authScheme.getHttpMessage(provider,
request.getRestMethod(), buildRestUrl(request), requestHeaders,
buildRestPayload(request));
HttpResponseMessage responseMessage = httpClient.execute(message);
logger.finest(buildLogRecord(request, responseMessage));
Response response = Response.parseRestResponse(request, responseMessage,
provider.getVersion());
return response;
}
String buildRpcUrl(Map<String, Request> requests) {
StringBuilder builder = new StringBuilder(provider.getRpcEndpoint());
// Remove trailing forward slash
if (builder.charAt(builder.length() - 1) == '/') {
builder.deleteCharAt(builder.length() - 1);
}
if (requests.size() == 1) {
appendQueryString(builder,
requests.values().iterator().next().getRpcQueryStringParameters());
}
return builder.toString();
}
byte[] buildRpcPayload(Map<String, Request> requests) {
if (requests.size() == 1 &&
requests.values().iterator().next().getCustomPayload() != null) {
return requests.values().iterator().next().getCustomPayload();
}
JSONArray requestArray = new JSONArray();
for (Map.Entry<String, Request> requestEntry : requests.entrySet()) {
JSONObject request = new JSONObject();
request.put("id", requestEntry.getKey());
request.put("method", requestEntry.getValue().getRpcMethod());
JSONObject requestParams = new JSONObject();
for (Map.Entry<String, Object> parameter :
requestEntry.getValue().getRpcPayloadParameters().entrySet()) {
requestParams.put(parameter.getKey(), parameter.getValue());
}
request.put("params", requestParams);
requestArray.add(request);
}
try {
return requestArray.toJSONString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
String buildRestUrl(Request request) {
StringBuilder builder = new StringBuilder(provider.getRestEndpoint());
String[] components = request.getRestUrlTemplate().split("/");
for (String component : components) {
if (component.startsWith("{") && component.endsWith("}")) {
String tag = component.substring(1, component.length()-1);
if (request.getComponent(tag) != null) {
builder.append(request.getComponent(tag));
builder.append("/");
}
} else {
builder.append(component);
builder.append("/");
}
}
// Remove trailing forward slash
builder.deleteCharAt(builder.length() - 1);
// Append query string parameters
Map<String, String> parameters = request.getRestQueryStringParameters();
appendQueryString(builder, parameters);
return builder.toString();
}
byte[] buildRestPayload(Request request) {
if (request.getCustomPayload() != null) {
return request.getCustomPayload();
}
Map<String, Object> parameters = request.getRestPayloadParameters();
if (parameters == null || parameters.size() == 0) {
return null;
}
JSONObject payload = new JSONObject();
for (Map.Entry<String, Object> parameter : parameters.entrySet()) {
payload.put(parameter.getKey(), parameter.getValue());
}
try {
return payload.toJSONString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
private void appendQueryString(StringBuilder builder,
Map<String, String> parameters) {
if (parameters != null && parameters.size() > 0) {
boolean runOnce = false;
for (Map.Entry<String, String> parameter: parameters.entrySet()) {
if (!runOnce) {
builder.append("?");
runOnce = true;
} else {
builder.append("&");
}
try {
builder.append(URLEncoder.encode(parameter.getKey(), "UTF-8"));
builder.append("=");
builder.append(URLEncoder.encode(parameter.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
// Ignore
}
}
}
}
private String buildLogRecord(Map<String, Request> requests,
HttpResponseMessage message) {
String payload = null;
byte[] bytes = buildRpcPayload(requests);
if (bytes != null) {
try {
payload = new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Ignore
}
}
return buildLogRecord(payload, message);
}
private String buildLogRecord(Request request, HttpResponseMessage message) {
String payload = null;
if (request.getCustomPayload() == null) {
byte[] bytes = buildRestPayload(request);
if (bytes != null) {
try {
payload = new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
// Ignore
}
}
}
return buildLogRecord(payload, message);
}
private String buildLogRecord(String payload, HttpResponseMessage message) {
StringBuilder builder = new StringBuilder("\n");
builder.append(message.getMethod());
builder.append("\n");
builder.append(message.getUrl().toString());
builder.append("\n");
if (payload != null) {
builder.append(payload);
builder.append("\n");
}
builder.append(message.getStatusCode());
builder.append("\n");
builder.append(message.getResponse());
return builder.toString();
}
}