/*
* Copyright 2010 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 com.samsung.appengine.jsonrpc.server;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import com.samsung.appengine.allshared.JsonRpcException;
import com.samsung.appengine.allshared.JsonRpcMethod;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
/**
* A base servlet class that handles JSON-RPC requests on Google App Engine. Extending classes
* define RPC methods by adding {@link JsonRpcMethod} annotations to methods. User login via
* Google Accounts is handled using App Engine's {@link UserService}.
*/
@SuppressWarnings("serial")
public class JsonRpcServlet extends HttpServlet {
private static final Logger log = Logger.getLogger(JsonRpcServlet.class.getName());
private static PersistenceManagerFactory pmfInstance;
private Map<String, Method> mMethods = new HashMap<String, Method>();
public JsonRpcServlet() {
for (Method method : getClass().getMethods()) {
JsonRpcMethod rpcMethodAnnotation = method.getAnnotation(JsonRpcMethod.class);
if (rpcMethodAnnotation != null) {
mMethods.put(rpcMethodAnnotation.method(), method);
}
}
}
public void init() {
/**
* Initialize PMF - we use a context attribute, so other servlets can
* be share the same instance. This is similar with a shared static
* field, but avoids dependencies.
*/
pmfInstance =
(PersistenceManagerFactory) getServletContext().getAttribute(
PersistenceManagerFactory.class.getName());
if (pmfInstance == null) {
pmfInstance = JDOHelper
.getPersistenceManagerFactory("transactions-optional");
getServletContext().setAttribute(
PersistenceManagerFactory.class.getName(),
pmfInstance);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
boolean debug = isDebug(req);
boolean prettyPrint = false;
JSONTokener tokener = new JSONTokener(req.getReader());
JSONObject requestJson;
JSONArray callsJson = null;
JSONObject responseJson = new JSONObject();
JSONArray resultsJson = new JSONArray();
PersistenceManager pm = pmfInstance.getPersistenceManager();
UserService userService = UserServiceFactory.getUserService();
CallContext context = new CallContext(req, null, pm, userService);
try {
try {
requestJson = new JSONObject(tokener);
if (requestJson.has("pretty") && requestJson.getBoolean("pretty"))
prettyPrint = true;
callsJson = requestJson.getJSONArray("calls");
} catch (JSONException e) {
responseJson.put("error", 400);
responseJson.put("message", "Error parsing request object: " + e.getMessage());
}
if (callsJson != null) {
for (int i = 0; i < callsJson.length(); i++) {
JSONObject callParamsJson = callsJson.getJSONObject(i);
context.setParams(callParamsJson);
JSONObject resultJson = new JSONObject();
try {
Object dataJson = performCall(context);
resultJson.put("data", (dataJson != null) ? dataJson : new JSONObject());
} catch (JsonRpcException e) {
if (debug && e.getHttpCode() != 403)
throw new RuntimeException(e);
resultJson.put("error", e.getHttpCode());
resultJson.put("message", e.getMessage());
log.log(Level.SEVERE,
"JsonRpcException (method: " + e.getMethodName() + ")", e);
}
resultsJson.put(resultJson);
}
}
responseJson.put("results", resultsJson);
resp.setContentType("application/json");
if (prettyPrint)
resp.getWriter().write(responseJson.toString(2) + "\n");
else
resp.getWriter().write(responseJson.toString() + "\n");
} catch (JSONException e) {
if (debug)
throw new RuntimeException(e);
resp.setStatus(500);
resp.setContentType("text/plain");
resp.getWriter().write("Internal JSON serialization error: " + e.getMessage());
log.log(Level.SEVERE, "JSONException", e);
} finally {
pm.close();
}
}
private Object performCall(CallContext context) throws JsonRpcException {
if (!context.getParams().has("method")) {
throw new JsonRpcException(400, "No method specified.");
}
Method method;
try {
String methodName = context.getParams().getString("method");
if (!mMethods.containsKey(methodName)) {
throw new JsonRpcException(400, "Unknown method.");
}
method = mMethods.get(methodName);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid method.");
}
JsonRpcMethod rpcMethodAnnotation = method.getAnnotation(JsonRpcMethod.class);
String methodName = rpcMethodAnnotation.method();
if (rpcMethodAnnotation.requires_login() && !context.getUserService().isUserLoggedIn()) {
throw new JsonRpcException(403, methodName,
"You must authenticate to run this RPC call.");
}
try {
Object data = method.invoke(this, context);
return data;
} catch (InvocationTargetException e) {
if (e.getCause() instanceof JsonRpcException) {
JsonRpcException e2 = (JsonRpcException) e.getCause();
e2.setMethodName(methodName);
throw e2;
} else if (e.getCause() instanceof JSONException) {
throw new JsonRpcException(500, methodName,
"Internal serialization error: " + e.getMessage(), e.getCause());
} else {
throw new JsonRpcException(500, methodName,
"Internal error: " + e.getMessage(), e.getCause());
}
} catch (IllegalArgumentException e) {
throw new JsonRpcException(500, methodName,
"Internal error: Illegal RPC call arguments.");
} catch (IllegalAccessException e) {
throw new JsonRpcException(500, methodName,
"Internal error: Illegal RPC access exception.");
}
}
protected boolean isDebug(HttpServletRequest req) {
return false;
}
public class CallContext {
private HttpServletRequest request;
private JSONObject params;
private PersistenceManager pm;
private UserService userService;
public CallContext(HttpServletRequest request, JSONObject params, PersistenceManager pm,
UserService userService) {
this.request = request;
this.params = params;
this.pm = pm;
this.userService = userService;
}
public HttpServletRequest getRequest() {
return request;
}
public JSONObject getParams() {
return params;
}
public void setParams(JSONObject params) {
this.params = params;
}
public PersistenceManager getPersistenceManager() {
return pm;
}
public UserService getUserService() {
return userService;
}
}
}