All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.shindig.protocol.JsonRpcServlet Maven / Gradle / Ivy

The newest version!
/*
 * 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.protocol;

import com.google.common.base.Strings;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
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.multipart.FormDataItem;
import org.apache.shindig.protocol.multipart.MultipartFormParser;

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;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;

/**
 * JSON-RPC handler servlet.
 */
public class JsonRpcServlet extends ApiServlet {

  public static final Set ALLOWED_CONTENT_TYPES =
      new ImmutableSet.Builder().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;
    }

    SecurityToken token = getSecurityToken(servletRequest);
    if (token == null) {
      sendSecurityError(servletResponse);
      return;
    }

    HttpUtil.setCORSheader(servletResponse, containerConfig.getList(token.getContainer(), "gadgets.parentOrigins"));

    try {
      String content = null;
      String callback = null; // for JSONP
      Map 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 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 formItems ,
      HttpServletRequest servletRequest, HttpServletResponse servletResponse,
      SecurityToken token, String callback) throws JSONException, IOException {
    // Use linked hash map to preserve order
    List> 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 result = new ArrayList(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 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 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 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 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 errorTitles = ImmutableMap. 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 error = new HashMap(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()));
  }
}