org.infoscoop.gadgets.servlet.JsonRpcServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.infoscoop.gadgets.servlet.JsonRpcServlet.java

Source

/* infoScoop OpenSource
 * Copyright (C) 2010 Beacon IT Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0-standalone.html>.
 */
package org.infoscoop.gadgets.servlet;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.shindig.auth.AbstractSecurityToken;
import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.common.servlet.HttpUtil;
import org.apache.shindig.common.util.JsonConversionUtil;
import org.apache.shindig.protocol.ApiServlet;
import org.apache.shindig.protocol.ContentTypes;
import org.apache.shindig.protocol.DataCollection;
import org.apache.shindig.protocol.ResponseItem;
import org.apache.shindig.protocol.RestfulCollection;
import org.apache.shindig.protocol.RpcHandler;
import org.apache.shindig.protocol.multipart.FormDataItem;
import org.apache.shindig.protocol.multipart.MultipartFormParser;
import org.infoscoop.acl.SecurityController;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.name.Named;

/**
 * JSON-RPC handler servlet.
 * It is greatly dependent on Shindig. 
 */
public class JsonRpcServlet extends ApiServlet {

    public static final Set<String> ALLOWED_CONTENT_TYPES = new ImmutableSet.Builder<String>()
            .addAll(ContentTypes.ALLOWED_JSON_CONTENT_TYPES).addAll(ContentTypes.ALLOWED_MULTIPART_CONTENT_TYPES)
            .build();

    /**
     * In a multipart request, the form item with field name "request" will contain the
     * actual request, per the proposed Opensocial 0.9 specification.
     */
    public static final String REQUEST_PARAM = "request";

    private MultipartFormParser formParser;

    @Inject
    void setMultipartFormParser(MultipartFormParser formParser) {
        this.formParser = formParser;
    }

    private String jsonRpcResultField = "result";
    private boolean jsonRpcBothFields = false;

    @Inject(optional = true)
    void setJsonRpcResultField(@Named("shindig.json-rpc.result-field") String jsonRpcResultField) {
        this.jsonRpcResultField = jsonRpcResultField;
        jsonRpcBothFields = "both".equals(jsonRpcResultField);
    }

    @Override
    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws IOException {
        setCharacterEncodings(servletRequest, servletResponse);
        servletResponse.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);

        // only GET/POST
        String method = servletRequest.getMethod();

        if (!("GET".equals(method) || "POST".equals(method))) {
            sendError(servletResponse,
                    new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "Only POST/GET Allowed"));
            return;
        }

        // Since the function which can be used is restricted, it seems that it is unnecessary. 
        /*
        SecurityToken token = getSecurityToken(servletRequest);
        if (token == null) {
          sendSecurityError(servletResponse);
          return;
        }
            
        HttpUtil.setCORSheader(servletResponse, containerConfig.<String>getList(token.getContainer(), "gadgets.parentOrigins"));
        */

        String appUrl = servletRequest.getHeader("x-is-gadgeturl");
        String containerUrl = servletRequest.getHeader("x-is-hostprefix");
        String moduleId = servletRequest.getHeader("x-is-moduleid");
        String uid = SecurityController.getPrincipalByType("UIDPrincipal").getName();
        SecurityToken token = new DefaultSecurityToken(containerUrl, appUrl, moduleId, uid, uid);

        try {
            String content = null;
            String callback = null; // for JSONP
            Map<String, FormDataItem> formData = Maps.newHashMap();

            // Get content or deal with JSON-RPC GET
            if ("POST".equals(method)) {
                content = getPostContent(servletRequest, formData);
            } else if (HttpUtil.isJSONP(servletRequest)) {
                content = servletRequest.getParameter("request");
                callback = servletRequest.getParameter("callback");
            } else {
                // GET request, fromRequest() creates the json objects directly.
                JSONObject request = JsonConversionUtil.fromRequest(servletRequest);

                if (request != null) {
                    dispatch(request, formData, servletRequest, servletResponse, token, null);
                    return;
                }
            }

            if (content == null) {
                sendError(servletResponse,
                        new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "No content specified"));
                return;
            }

            if (isContentJsonBatch(content)) {
                JSONArray batch = new JSONArray(content);
                dispatchBatch(batch, formData, servletRequest, servletResponse, token, callback);
            } else {
                JSONObject request = new JSONObject(content);
                dispatch(request, formData, servletRequest, servletResponse, token, callback);
            }
        } catch (JSONException je) {
            sendJsonParseError(je, servletResponse);
        } catch (IllegalArgumentException e) {
            // a bad jsonp request..
            sendBadRequest(e, servletResponse);
        } catch (ContentTypes.InvalidContentTypeException icte) {
            sendBadRequest(icte, servletResponse);
        }
    }

    protected String getPostContent(HttpServletRequest request, Map<String, FormDataItem> formItems)
            throws ContentTypes.InvalidContentTypeException, IOException {
        String content = null;

        ContentTypes.checkContentTypes(ALLOWED_CONTENT_TYPES, request.getContentType());

        if (formParser.isMultipartContent(request)) {
            for (FormDataItem item : formParser.parse(request)) {
                if (item.isFormField() && REQUEST_PARAM.equals(item.getFieldName()) && content == null) {
                    // As per spec, in case of a multipart/form-data content, there will be one form field
                    // with field name as "request". It will contain the json request. Any further form
                    // field or file item will not be parsed out, but will be exposed via getFormItem
                    // method of RequestItem.
                    if (!Strings.isNullOrEmpty(item.getContentType())) {
                        ContentTypes.checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES,
                                item.getContentType());
                    }
                    content = item.getAsString();
                } else {
                    formItems.put(item.getFieldName(), item);
                }
            }
        } else {
            content = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
        }
        return content;
    }

    protected void dispatchBatch(JSONArray batch, Map<String, FormDataItem> formItems,
            HttpServletRequest servletRequest, HttpServletResponse servletResponse, SecurityToken token,
            String callback) throws JSONException, IOException {
        // Use linked hash map to preserve order
        List<Future<?>> responses = Lists.newArrayListWithCapacity(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);
            responses.add(getHandler(batchObj, servletRequest).execute(formItems, token, jsonConverter));
        }

        // Resolve each Future into a response.
        // TODO: should use shared deadline across each request
        List<Object> result = new ArrayList<Object>(batch.length());
        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.add(getJSONResponse(key, getResponseItem(responses.get(i))));
        }

        // Generate the output
        Writer writer = servletResponse.getWriter();
        if (callback != null)
            writer.append(callback).append('(');
        jsonConverter.append(writer, result);
        if (callback != null)
            writer.append(");\n");
    }

    protected void dispatch(JSONObject request, Map<String, FormDataItem> formItems,
            HttpServletRequest servletRequest, HttpServletResponse servletResponse, SecurityToken token,
            String callback) throws JSONException, IOException {
        String key = null;

        if (request.has("id")) {
            key = request.getString("id");
        }

        // getRpcHandler never returns null
        Future<?> future = getHandler(request, servletRequest).execute(formItems, token, jsonConverter);

        // Resolve each Future into a response.
        // TODO: should use shared deadline across each request
        ResponseItem response = getResponseItem(future);
        Object result = getJSONResponse(key, response);

        // Generate the output
        Writer writer = servletResponse.getWriter();
        if (callback != null)
            writer.append(callback).append('(');
        jsonConverter.append(writer, result);
        if (callback != null)
            writer.append(");\n");
    }

    /**
     * 
     */
    protected void addResult(Map<String, Object> result, Object data) {
        if (jsonRpcBothFields) {
            result.put("result", data);
            result.put("data", data);
        } else {
            result.put(jsonRpcResultField, data);
        }
    }

    /**
     * Determine if the content contains a batch request
     *
     * @param content json content or null
     * @return true if content contains is a json array, not a json object or null
     */
    private boolean isContentJsonBatch(String content) {
        if (content == null)
            return false;
        return ((content.indexOf('[') != -1) && content.indexOf('[') < content.indexOf('{'));
    }

    /**
     * Wrap call to dispatcher to allow for implementation specific overrides
     * and servlet-request contextual handling
     */
    protected RpcHandler getHandler(JSONObject rpc, HttpServletRequest request) {
        return dispatcher.getRpcHandler(rpc);
    }

    Object getJSONResponse(String key, ResponseItem responseItem) {
        Map<String, Object> result = Maps.newHashMap();
        if (key != null) {
            result.put("id", key);
        }
        if (responseItem.getErrorCode() < 200 || responseItem.getErrorCode() >= 400) {
            result.put("error", getErrorJson(responseItem));
        } else {
            Object response = responseItem.getResponse();
            if (response instanceof DataCollection) {
                addResult(result, ((DataCollection) response).getEntry());
            } else if (response instanceof RestfulCollection) {
                Map<String, Object> map = Maps.newHashMap();
                RestfulCollection<?> collection = (RestfulCollection<?>) response;
                // Return sublist info
                if (collection.getTotalResults() != collection.getEntry().size()) {
                    map.put("startIndex", collection.getStartIndex());
                    map.put("itemsPerPage", collection.getItemsPerPage());
                }
                // always put in totalResults
                map.put("totalResults", collection.getTotalResults());

                if (!collection.isFiltered())
                    map.put("filtered", collection.isFiltered());

                if (!collection.isUpdatedSince())
                    map.put("updatedSince", collection.isUpdatedSince());

                if (!collection.isSorted())
                    map.put("sorted", collection.isUpdatedSince());

                map.put("list", collection.getEntry());
                addResult(result, map);
            } else {
                addResult(result, response);
            }

            // TODO: put "code" for != 200?
        }
        return result;
    }

    /** Map of old-style error titles */
    private static final Map<Integer, String> errorTitles = ImmutableMap.<Integer, String>builder()
            .put(HttpServletResponse.SC_NOT_IMPLEMENTED, "notImplemented")
            .put(HttpServletResponse.SC_UNAUTHORIZED, "unauthorized")
            .put(HttpServletResponse.SC_FORBIDDEN, "forbidden")
            .put(HttpServletResponse.SC_BAD_REQUEST, "badRequest")
            .put(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internalError")
            .put(HttpServletResponse.SC_EXPECTATION_FAILED, "limitExceeded").build();

    // 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 Object getErrorJson(ResponseItem responseItem) {
        Map<String, Object> error = new HashMap<String, Object>(2, 1);
        error.put("code", responseItem.getErrorCode());

        String message = errorTitles.get(responseItem.getErrorCode());
        if (message == null) {
            message = responseItem.getErrorMessage();
        } else {
            if (StringUtils.isNotBlank(responseItem.getErrorMessage())) {
                message += ": " + responseItem.getErrorMessage();
            }
        }

        if (StringUtils.isNotBlank(message)) {
            error.put("message", message);
        }

        if (responseItem.getResponse() != null) {
            error.put("data", responseItem.getResponse());
        }

        return error;
    }

    @Override
    protected void sendError(HttpServletResponse servletResponse, ResponseItem responseItem) throws IOException {
        jsonConverter.append(servletResponse.getWriter(), getErrorJson(responseItem));

        servletResponse.setStatus(responseItem.getErrorCode());
    }

    private void sendBadRequest(Throwable t, HttpServletResponse response) throws IOException {
        sendError(response,
                new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "Invalid input - " + t.getMessage()));
    }

    private void sendJsonParseError(JSONException e, HttpServletResponse response) throws IOException {
        sendError(response,
                new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "Invalid JSON - " + e.getMessage()));
    }

    class DefaultSecurityToken extends AbstractSecurityToken {

        public DefaultSecurityToken(String container, String appUrl, String appId, String viewerId,
                String ownerId) {
            super.setContainer(container);
            super.setAppUrl(appUrl);
            super.setAppId(appId);
            super.setViewerId(viewerId);
            super.setOwnerId(ownerId);
        }

        public String getUpdatedToken() {
            // TODO Auto-generated method stub
            return null;
        }

        public String getAuthenticationMode() {
            // TODO Auto-generated method stub
            return null;
        }

        public boolean isAnonymous() {
            // TODO Auto-generated method stub
            return false;
        }

        @Override
        protected EnumSet<Keys> getMapKeys() {
            // TODO Auto-generated method stub
            return null;
        }

    }
}