/* * Copyright (c) 2012 Lockheed Martin Corporation * * 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.eurekastreams.web.services; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.ObjectNode; import org.eurekastreams.commons.actions.context.ClientPrincipalActionContextImpl; import org.eurekastreams.commons.actions.context.Principal; import org.eurekastreams.commons.actions.context.PrincipalActionContext; import org.eurekastreams.commons.actions.service.ServiceAction; import org.eurekastreams.commons.actions.service.TaskHandlerServiceAction; import org.eurekastreams.commons.exceptions.InvalidActionException; import org.eurekastreams.commons.exceptions.SessionException; import org.eurekastreams.commons.logging.LogFactory; import org.eurekastreams.commons.server.service.ActionController; import org.eurekastreams.server.persistence.mappers.cache.Transformer; import org.eurekastreams.server.service.restlets.support.JsonFieldObjectExtractor; import org.hibernate.bytecode.buildtime.ExecutionException; import org.restlet.data.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; /** * Provides a "rest-like" API for Eureka Streams allowing access to the action framework. */ @Controller public class ActionApiController { /** Log. */ private final Logger log = LoggerFactory.getLogger(LogFactory.getClassName()); /** The context from which this service can load action beans. */ private final BeanFactory beanFactory; /** Service Action Controller. */ private final ActionController serviceActionController; /** Prepares exceptions for returning to the client. */ private final Transformer<Exception, Exception> exceptionSanitizer; /** Principal populators. */ private final List<Transformer<Request, Principal>> principalExtractors; /** Client populators. */ private final List<Transformer<Request, String>> clientExtractors; /** JSON Factory for building JSON Generators. */ private final JsonFactory jsonFactory; /** Only allow read-only actions. */ private final boolean readOnly; /** Extracts request from the parameters block. */ private final JsonFieldObjectExtractor jsonFieldObjectExtractor; /** Action types map. */ private final Map<String, String> actionTypes; /** Action rewrite name map. */ private final Map<String, String> actionRewrites; /** If the session should be verified. */ private boolean verifySession = true; /** JSON object mapper. */ private final ObjectMapper jsonObjectMapper; /** List of fields to be excluded when serializing an exception. */ private Collection<String> exceptionFieldBlackList; /** * Default constructor. * * @param inServiceActionController * the action controller. * @param inExceptionSanitizer * Prepares exceptions for returning to the client. * @param inPrincipalExtractors * the principal extractors. * @param inClientExtractors * Strategies to extract the client. * @param inJsonFactory * the json factory. * @param inJsonObjectMapper * JSON object mapper. * @param inJsonFieldObjectExtractor * Extracts request from the parameters block. * @param inReadOnly * Only allow read-only actions. * @param inActionTypes * action types map. * @param inActionRewrites * action rewrite map. * @param inVerifySession * if the session should be verified. * @param inBeanFactory * The context from which this service can load action beans. */ public ActionApiController(final ActionController inServiceActionController, final Transformer<Exception, Exception> inExceptionSanitizer, final List<Transformer<Request, Principal>> inPrincipalExtractors, final List<Transformer<Request, String>> inClientExtractors, final JsonFactory inJsonFactory, final ObjectMapper inJsonObjectMapper, final JsonFieldObjectExtractor inJsonFieldObjectExtractor, final boolean inReadOnly, final Map<String, String> inActionTypes, final Map<String, String> inActionRewrites, final boolean inVerifySession, final BeanFactory inBeanFactory) { serviceActionController = inServiceActionController; exceptionSanitizer = inExceptionSanitizer; principalExtractors = inPrincipalExtractors; clientExtractors = inClientExtractors; jsonFactory = inJsonFactory; jsonObjectMapper = inJsonObjectMapper; jsonFieldObjectExtractor = inJsonFieldObjectExtractor; readOnly = inReadOnly; actionTypes = inActionTypes; actionRewrites = inActionRewrites; verifySession = inVerifySession; beanFactory = inBeanFactory; generateExceptionFieldBlackList(); } /** * Executes a single action from the action framework as requested by the API. * * @param apiName * API action name. * @param claimedSessionId * Session ID provided by the client (for XSRF prevention). * @param parameters * Request parameter data as a JSON string. * @param request * HTTP request. * @param response * HTTP response. * @throws IOException * Only if setting an HTTP error code throws an error. */ @RequestMapping(value = "executeSingle", method = RequestMethod.POST) public void executeSingle(@RequestParam(value = "apiName", required = true) final String apiName, @RequestParam(value = "sessionId", required = true) final String claimedSessionId, @RequestParam(value = "parameters", required = true) final String parameters, final HttpServletRequest request, final HttpServletResponse response) throws IOException { try { // do the main work Serializable result = coreExecuteSingle(apiName, claimedSessionId, parameters, request); // write headers - prevent caching response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); response.addHeader("Pragma", "no-cache"); // serialize ActionApiTransport container = new ActionApiTransport(); JsonGenerator jsonGenerator = jsonFactory.createJsonGenerator(response.getWriter()); if (result instanceof Exception) { Exception ex = (Exception) result; final Exception exClean = exceptionSanitizer.transform(ex); log.error("Error performing action via API. Will return sanitized exception.", ex); container.setSuccess(false); container.setResult(exClean); // modify the exception to suit how we want to serialize it JsonNode tree = jsonObjectMapper.valueToTree(container); ObjectNode resultNode = (ObjectNode) tree.get("result"); resultNode.remove(exceptionFieldBlackList); resultNode.put("type", exClean.getClass().getName()); jsonObjectMapper.writeTree(jsonGenerator, tree); } else { container.setSuccess(true); container.setResult(result); jsonObjectMapper.writeValue(jsonGenerator, container); } } catch (Exception ex) { log.error("Error performing action via API. Will return HTTP error.", ex); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } /** * Core logic for executeSingle. * * @param apiName * API action name. * @param claimedSessionId * Session ID provided by the client (for XSRF prevention). * @param parameters * Request parameter data as a JSON string. * @param request * HTTP request. * @return The object to return to the client: action result data or an exception. */ private Serializable coreExecuteSingle(final String apiName, final String claimedSessionId, final String parameters, final HttpServletRequest request) { // verify session if (verifySession) { // get real session HttpSession session = request.getSession(); if (session == null) { return new SessionException("Request has no session."); } String realSessionId = session.getId(); if (StringUtils.isBlank(realSessionId)) { return new SessionException("Request session has no valid ID."); } // compare with claimed session if (!realSessionId.equals(claimedSessionId)) { log.error("Provided session ID '{}' does not match request session ID '{}'.", claimedSessionId, realSessionId); return new SessionException("Provided session ID does not match request session ID."); } } // get the principal (throws if none available) Principal principal = getPrincipal(request); // Skip determining the client - this flavor of the API is intended for preauth, so it would be directly // accessed by a user's app and thus the client would not apply. (Client is used for OAuth scenarios where // an app is accessing the API on behalf of a user.) When this code is updated/refactored to fully replace // ActionResource (the Noelios version of the API), then determining the client will need to be done. // determine request type String actionName = actionRewrites.containsKey(apiName) ? actionRewrites.get(apiName) : apiName; String requestType = actionTypes.get(actionName); if (requestType == null) { return new InvalidActionException(String.format("Request for unknown API '%s'.", actionName)); } try { // get action parameter Serializable actionParameter = getRequestObject(parameters, requestType); // execute the action return performAction(actionName, actionParameter, principal, ""); } catch (Exception ex) { return ex; } } /** * Returns Principal for given account id. * * @param inRequest * Request to get principal from. * @return Principal for given account id. */ private Principal getPrincipal(final HttpServletRequest inRequest) { log.debug("Attempting to retrieve principal"); for (Transformer<Request, Principal> extractor : principalExtractors) { // NOTE: Passing null because the extractors still use a Noelios request instead of a servlet request, so it // would not be compatible. This is ok, because the extractor used by this endpoint doesn't look at the // request anyway. Principal result = extractor.transform(null); if (result != null) { return result; } } throw new RuntimeException("No principal found"); } /** * Go from JSON to Request object. * * @param parameters * Request parameter data as a JSON string. * @param requestType * Name of the Java data type described by the request data. * * @return the request object. * @throws Exception * possible exceptions. */ private Serializable getRequestObject(final String parameters, final String requestType) throws Exception { if (requestType.toLowerCase().equals("null")) { return null; } String parametersDecoded = parameters; // URLDecoder.decode(parameters, "UTF-8"); final JSONObject paramsAsObject = JSONObject.fromObject(parametersDecoded); return (Serializable) jsonFieldObjectExtractor.extract(paramsAsObject, "request", requestType); } /** * Actually performs the action. * * @param actionName * Name of action bean. * @param actionParameter * Parameter to the action bean. * @param principal * Principal. * @param clientUniqueId * Unique ID for the source which sent the request (not presently used - will be of use when the OAuth * endpoints are moved to this class). * @return The action result. */ private Serializable performAction(final String actionName, final Serializable actionParameter, final Principal principal, final String clientUniqueId) { // get the action Object springBean = beanFactory.getBean(actionName); // create context PrincipalActionContext actionContext = new ClientPrincipalActionContextImpl(actionParameter, principal, clientUniqueId); actionContext.setActionId(actionName); log.debug("Executing action {} for user {}.", actionName, principal.getAccountId()); // execute (or not) based on type of bean if (springBean instanceof ServiceAction) { ServiceAction action = (ServiceAction) springBean; if (readOnly && !action.isReadOnly()) { throw new ExecutionException(String.format("Action '%s' is not read-only.", actionName)); } return serviceActionController.execute(actionContext, action); } else if (springBean instanceof TaskHandlerServiceAction) { TaskHandlerServiceAction action = (TaskHandlerServiceAction) springBean; if (readOnly && !action.isReadOnly()) { throw new ExecutionException(String.format("Action '%s' is not read-only.", actionName)); } return serviceActionController.execute(actionContext, action); } else if (springBean == null) { throw new InvalidActionException(String.format("Unknown bean '%s'.", actionName)); } else { throw new InvalidActionException(String.format("Bean '%s' is not an action.", actionName)); } } /** * Build the list of fields to be excluded when serializing an exception. */ private void generateExceptionFieldBlackList() { // get list of fields in a basic Exception, use them all except 'message' JsonNode tree = jsonObjectMapper.valueToTree(new Exception()); exceptionFieldBlackList = new ArrayList<String>(tree.size()); Iterator<String> iter = tree.getFieldNames(); while (iter.hasNext()) { String fn = iter.next(); if (!"message".equals(fn)) { exceptionFieldBlackList.add(fn); } } } }