package org.webcat.core.webapi; import java.io.StringWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.apache.http.HttpStatus; import org.apache.log4j.Logger; import org.webcat.core.EOBase; import org.webcat.core.EntityRequestInfo; import org.webcat.core.Session; import org.webcat.core.User; import org.webcat.core.http.MetaRequestHandler; import org.webcat.core.http.RequestHandlerWithResponse; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSMutableDictionary; //------------------------------------------------------------------------- /** * <p> * The base class for a web API controller, inspired by controllers in Ruby on * Rails. This class should be subclassed by subsystems that want to provide a * RESTful API for some of their objects. * </p><p> * Like direct actions in WebObjects, web API controllers do not need to be * explicitly registered with any application component. If a request of the * format <code>Web-CAT.woa/api/[EntityName]/[id]/[action]</code> is made, then * the system will automatically search in any package for a class named * <code>[EntityName]WebAPIController</code>, create an instance of it if * necessary, and then call the appropriate action method. * </p><p> * Methods that handle actions should be named "[...]Action", where [...] is * the name of the action to be handled. If the action acts on a collection of * objects, then it should be parameterless (it is up to the action method to * determine which collection to fetch). If the action acts on a single object, * then it should take a single parameter that is a reference to that object. * For example: * </p> * <ul> * <li><code>.../api/Course/foo</code> calls * <code>CourseWebAPIController.fooAction()</code></li> * <li><code>.../api/Course/5/foo</code> calls * <code>CourseWebAPIController.fooAction(Course)</code></li> * </ul> * <p> * If no action name is specified, then <code>"index"</code> is assumed for * collections and <code>"show"</code> is assumed for objects: * </p> * <ul> * <li><code>.../api/Course</code> calls * <code>CourseWebAPIController.indexAction()</code></li> * <li><code>.../api/Course/5</code> calls * <code>CourseWebAPIController.showAction(Course)</code></li> * </ul> * <p> * The following action methods are reserved. Aside from these, subclasses can * provide their own arbitrary methods to handle custom actions: * </p> * <ul> * <li><code>indexAction():</code> a GET request on a collection, * used to list resources</li> * <li><code>showAction(Object):</code> a GET request on an object, * used to retrieve a resource</li> * <li><code>createAction():</code> a POST request on a collection, * used to create new resources</li> * <li><code>updateAction(Object):</code> a PUT request on an object, * used to update resources</li> * <li><code>deleteAction(Object):</code> a DELETE request on an object, * used to delete a resource</li> * </ul> * <p> * In the event that the API identifier of an object and an action might * collide, then the action is assumed to be a collection action with that name * rather than an object action with that ID. * </p> * * @author Tony Allevato * @author Last changed by $Author: aallowat $ * @version $Revision: 1.1 $, $Date: 2012/06/22 16:23:17 $ */ public abstract class WebAPIController implements RequestHandlerWithResponse { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initializes a new {@code WebAPIController}. */ public WebAPIController() { cacheActionMethods(); initializeFormatters(); } //~ Methods ............................................................... // ---------------------------------------------------------- /** * Handles the request. Subclasses should not override this method; they * should write custom action methods as described in the Javadoc for this * class. * * @param request the request * @param response the response */ public void handleRequest(WORequest request, WOResponse response) { try { this.request = request; this.response = response; session = (Session) request.context().session(); editingContext = session.defaultEditingContext(); Object result = dispatchAction(); if (response.status() < 400) { formatResult(result); } } catch (Exception e) { log.error("(500) There was an error performing the action", e); response.setContent(""); response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); } } // ---------------------------------------------------------- /** * A convenience method to produce a 403 (Forbidden) response. */ public void forbid() { response.setContent(""); response.setStatus(HttpStatus.SC_FORBIDDEN); } // ---------------------------------------------------------- /** * Gets the request associated with the current action. * * @return the request */ public WORequest request() { return request; } // ---------------------------------------------------------- /** * Gets the response associated with the current action. * * @return the response */ public WOResponse response() { return response; } // ---------------------------------------------------------- /** * Gets the session associated with the current action. * * @return the session */ public Session session() { return session; } // ---------------------------------------------------------- /** * Gets the user who requested the current action. * * @return the user */ public User user() { return session.primeUser(); } // ---------------------------------------------------------- /** * Gets the session's editing context that can be used to fetch objects * during the current action. * * @return the editing context */ public EOEditingContext editingContext() { return editingContext; } // ---------------------------------------------------------- /** * Cache the reflection method handles for the actions in the class that * subclasses this controller class. */ private void cacheActionMethods() { collectionActions = new NSMutableDictionary<String, Method>(); objectActions = new NSMutableDictionary<String, Method>(); for (Method method : getClass().getMethods()) { String methodName = method.getName(); if (methodName.endsWith(ACTION_SUFFIX)) { String actionName = methodName.substring(0, methodName.length() - ACTION_SUFFIX.length()); Class<?>[] params = method.getParameterTypes(); if (params.length == 0) { collectionActions.setObjectForKey(method, actionName); } else if (params.length == 1) { objectActions.setObjectForKey(method, actionName); } } } } // ---------------------------------------------------------- /** * Initializes the formatters that can be used for responses from actions. */ private void initializeFormatters() { formatters = new NSMutableDictionary<String, ResponseFormatter>(); formatters.setObjectForKey(new XmlResponseFormatter(), "xml"); formatters.setObjectForKey(new JSONResponseFormatter(), "json"); } // ---------------------------------------------------------- /** * Dispatches the action by calling the appropriate method on the concrete * controller class. * * @throws InvocationTargetException if the action method throws an * exception * @throws IllegalAccessException if the action method is not public */ private Object dispatchAction() throws InvocationTargetException, IllegalAccessException { String path = request.requestHandlerPath(); EntityRequestInfo info = EntityRequestInfo.fromRequestHandlerPath(path, true); String actionOrId = info.objectID(); if (actionOrId == null) { Method method = collectionActions.objectForKey( defaultCollectionActionForRequest()); if (method != null) { return method.invoke(this); } } else { // If an action and an ID happen to collide, treat it as a // collection action rather than an object action, because this is // more efficient (it doesn't require a database hit to find this // out). Method method = collectionActions.objectForKey(actionOrId); if (method != null) { return method.invoke(this); } else { String action = info.resourcePath(); if (action == null) { action = defaultObjectActionForRequest(); } method = objectActions.objectForKey(action); if (method != null) { EOBase object = info.requestedObject(editingContext()); if (object == null || !object.accessibleByUser(user())) { forbid(); } else { return method.invoke(this, object); } } } } return null; } // ---------------------------------------------------------- /** * Formats the result of the action into the handler's response. * * @param result the result of the action * @throws Exception if an error occurs */ private void formatResult(Object result) throws Exception { ResponseFormatter formatter = null; NSArray<String> groups = (NSArray<String>) request.userInfoForKey( MetaRequestHandler.REGEX_CAPTURE_GROUPS_KEY); if (groups.size() > WebAPIRequestHandler.FORMAT_CAPTURE_GROUP && groups.objectAtIndex( WebAPIRequestHandler.FORMAT_CAPTURE_GROUP).length() > 0) { String format = groups.objectAtIndex( WebAPIRequestHandler.FORMAT_CAPTURE_GROUP).substring(1); formatter = formatters.objectForKey(format); } // Use JSON by default if no format specifier is given. if (formatter == null) { formatter = new JSONResponseFormatter(); } formatter.setResult(session().sessionID(), result); StringWriter writer = new StringWriter(); formatter.formatToWriter(writer); response.setContent(writer.toString()); } // ---------------------------------------------------------- /** * Gets the default action name for requests on collections based on the * HTTP method used in the request. * * @return the default action name */ private String defaultCollectionActionForRequest() { String method = request().method().toUpperCase(); if (method.equals("POST")) { return CREATE_ACTION; } else { return INDEX_ACTION; } } // ---------------------------------------------------------- /** * Gets the default action name for requests on individual objects based on * the HTTP method used in the request. * * @return the default action name */ private String defaultObjectActionForRequest() { String method = request().method().toUpperCase(); if (method.equals("PUT")) { return UPDATE_ACTION; } else if (method.equals("DELETE")) { return DELETE_ACTION; } else { return SHOW_ACTION; } } //~ Static/instance variables ............................................. private static Logger log = Logger.getLogger(WebAPIController.class); private static final String ACTION_SUFFIX = "Action"; private static final String INDEX_ACTION = "index"; private static final String CREATE_ACTION = "create"; private static final String SHOW_ACTION = "show"; private static final String UPDATE_ACTION = "update"; private static final String DELETE_ACTION = "delete"; private WORequest request; private WOResponse response; private Session session; private EOEditingContext editingContext; private NSMutableDictionary<String, Method> collectionActions; private NSMutableDictionary<String, Method> objectActions; private NSMutableDictionary<String, ResponseFormatter> formatters; }