/*
* 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 org.apache.shindig.social.opensocial.service;
import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.util.JsonConversionUtil;
import org.apache.shindig.social.ResponseError;
import org.apache.shindig.social.opensocial.spi.DataCollection;
import org.apache.shindig.social.opensocial.spi.RestfulCollection;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Future;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* JSON-RPC handler servlet.
*/
public class JsonRpcServlet extends ApiServlet {
@Override
protected void doGet(HttpServletRequest servletRequest,
HttpServletResponse servletResponse)
throws ServletException, IOException {
SecurityToken token = getSecurityToken(servletRequest);
if (token == null) {
sendSecurityError(servletResponse);
return;
}
try {
setCharacterEncodings(servletRequest, servletResponse);
JSONObject request = JsonConversionUtil.fromRequest(servletRequest);
dispatch(request, servletRequest, servletResponse, token);
} catch (JSONException je) {
// FIXME
}
}
@Override
protected void doPost(HttpServletRequest servletRequest,
HttpServletResponse servletResponse)
throws ServletException, IOException {
SecurityToken token = getSecurityToken(servletRequest);
if (token == null) {
sendSecurityError(servletResponse);
return;
}
setCharacterEncodings(servletRequest, servletResponse);
servletResponse.setContentType("application/json");
try {
String content = IOUtils.toString(servletRequest.getInputStream(),
servletRequest.getCharacterEncoding());
if ((content.indexOf('[') != -1) && content.indexOf('[') < content.indexOf('{')) {
// Is a batch
JSONArray batch = new JSONArray(content);
dispatchBatch(batch, servletRequest, servletResponse, token);
} else {
JSONObject request = new JSONObject(content);
dispatch(request, servletRequest, servletResponse, token);
}
} catch (JSONException je) {
sendBadRequest(je, servletResponse);
}
}
protected void dispatchBatch(JSONArray batch, HttpServletRequest servletRequest,
HttpServletResponse servletResponse, SecurityToken token) throws JSONException, IOException {
// Use linked hash map to preserve order
List<Future<?>> responses = Lists.newArrayListWithExpectedSize(batch.length());
// Gather all Futures. We do this up front so that
// the first call to get() comes after all futures are created,
// which allows for implementations that batch multiple Futures
// into single requests.
for (int i = 0; i < batch.length(); i++) {
JSONObject batchObj = batch.getJSONObject(i);
RpcRequestItem requestItem = new RpcRequestItem(batchObj, token, jsonConverter);
responses.add(handleRequestItem(requestItem, servletRequest));
}
// Resolve each Future into a response.
// TODO: should use shared deadline across each request
JSONArray result = new JSONArray();
for (int i = 0; i < batch.length(); i++) {
JSONObject batchObj = batch.getJSONObject(i);
String key = null;
if (batchObj.has("id")) {
key = batchObj.getString("id");
}
result.put(getJSONResponse(key, getResponseItem(responses.get(i))));
}
servletResponse.getWriter().write(result.toString());
}
protected void dispatch(JSONObject request, HttpServletRequest servletRequest,
HttpServletResponse servletResponse, SecurityToken token) throws JSONException, IOException {
String key = null;
if (request.has("id")) {
key = request.getString("id");
}
RpcRequestItem requestItem = new RpcRequestItem(request, token, jsonConverter);
// Resolve each Future into a response.
// TODO: should use shared deadline across each request
ResponseItem response = getResponseItem(handleRequestItem(requestItem, servletRequest));
JSONObject result = getJSONResponse(key, response);
servletResponse.getWriter().write(result.toString());
}
private JSONObject getJSONResponse(String key, ResponseItem responseItem) throws JSONException {
JSONObject result = new JSONObject();
if (key != null) {
result.put("id", key);
}
if (responseItem.getError() != null) {
result.put("error", getErrorJson(responseItem));
} else {
Object response = responseItem.getResponse();
JSONObject converted = (JSONObject) jsonConverter.convertToJson(response);
if (response instanceof RestfulCollection) {
// FIXME this is a little hacky because of the field names in the RestfulCollection
converted.put("list", converted.remove("entry"));
result.put("data", converted);
} else if (response instanceof DataCollection) {
if (converted.has("entry")) {
result.put("data", converted.get("entry"));
}
} else {
result.put("data", converted);
}
}
return result;
}
// TODO(doll): Refactor the responseItem so that the fields on it line up with this format.
// Then we can use the general converter to output the response to the client and we won't
// be harcoded to json.
private JSONObject getErrorJson(ResponseItem responseItem) throws JSONException {
JSONObject error = new JSONObject();
error.put("code", responseItem.getError().getHttpErrorCode());
String message = responseItem.getError().toString();
if (StringUtils.isNotBlank(responseItem.getErrorMessage())) {
message += ": " + responseItem.getErrorMessage();
}
error.put("message", message);
return error;
}
@Override
protected void sendError(HttpServletResponse servletResponse, ResponseItem responseItem)
throws IOException {
try {
JSONObject error = getErrorJson(responseItem);
servletResponse.getWriter().write(error.toString());
} catch (JSONException je) {
// This really shouldn't ever happen
servletResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Error generating error response " + je.getMessage());
}
}
private void sendBadRequest(Throwable t, HttpServletResponse response) throws IOException {
sendError(response, new ResponseItem(ResponseError.BAD_REQUEST,
"Invalid batch - " + t.getMessage()));
}
}