package er.rest.routes; import java.io.FileNotFoundException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOAction; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WODirectAction; import com.webobjects.appserver.WOMessage; import com.webobjects.appserver.WOPageNotFoundException; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOObjectStore; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSSet; import com.webobjects.foundation.NSValidation; import com.webobjects.foundation._NSUtilities; import er.extensions.appserver.ERXHttpStatusCodes; import er.extensions.appserver.ERXRequest; import er.extensions.appserver.ERXResponse; import er.extensions.eof.ERXDatabaseContextDelegate.ObjectNotAvailableException; import er.extensions.eof.ERXEC; import er.extensions.eof.ERXKey; import er.extensions.eof.ERXKeyFilter; import er.extensions.foundation.ERXExceptionUtilities; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXStringUtilities; import er.extensions.localization.ERXLocalizer; import er.extensions.validation.ERXValidationException; import er.rest.ERXBasicAuthenticationException; import er.rest.ERXNotAllowedException; import er.rest.ERXRequestFormValues; import er.rest.ERXRestClassDescriptionFactory; import er.rest.ERXRestContext; import er.rest.ERXRestFetchSpecification; import er.rest.ERXRestRequestNode; import er.rest.ERXRestUtils; import er.rest.format.ERXRestFormat; import er.rest.format.ERXWORestRequest; import er.rest.format.ERXWORestResponse; import er.rest.format.IERXRestParser; import er.rest.routes.jsr311.CookieParam; import er.rest.routes.jsr311.HeaderParam; import er.rest.routes.jsr311.Path; import er.rest.routes.jsr311.PathParam; import er.rest.routes.jsr311.Paths; import er.rest.routes.jsr311.QueryParam; import er.rest.util.ERXRestSchema; import er.rest.util.ERXRestTransactionRequestAdaptor; /** * ERXRouteController is equivalent to a Rails controller class. It's actually a direct action, and has the same naming * rules as a direct action, so your controller action methods must end in the name "Action". There are several utility * methods for manipulating restful requests and responses (update(..), create(..), requestNode(), response(..), etc) , * and it supports multiple formats for you. * * @property ERXRest.accessControlAllowRequestHeaders See https://developer.mozilla.org/En/HTTP_access_control#Access-Control-Allow-Headers * @property ERXRest.accessControlAllowRequestMethods See https://developer.mozilla.org/En/HTTP_access_control#Access-Control-Allow-Methods * @property ERXRest.defaultFormat (default "xml") Allow you to set the default format for all of your REST controllers * @property ERXRest.strictMode (default "true") If set to true, status code in the response will be 405 Not Allowed, if set to false, status code will be 404 Not Found * @property ERXRest.allowWindowNameCrossDomainTransport * @property ERXRest.accessControlMaxAge (default 1728000) This header indicates how long the results of a preflight request can be cached. See https://developer.mozilla.org/En/HTTP_access_control#Access-Control-Max-Age * @property ERXRest.accessControlAllowOrigin Set the value to '*' to enable all origins. See https://developer.mozilla.org/En/HTTP_access_control#Access-Control-Allow-Origin * * @author mschrag */ public class ERXRouteController extends WODirectAction { private static final Logger log = LoggerFactory.getLogger(ERXRouteController.class); private ERXRouteRequestHandler _requestHandler; private ERXRoute _route; private String _entityName; private ERXRestFormat _format; private NSDictionary<ERXRoute.Key, String> _routeKeys; private NSDictionary<ERXRoute.Key, Object> _objects; private EOEditingContext _editingContext; private ERXRestRequestNode _requestNode; private NSKeyValueCoding _options; private NSSet<String> _prefetchingKeyPaths; private boolean _shouldDisposeEditingContext; private ERXRestContext _restContext; /** * Constructs a new ERXRouteController. * * @param request * the request */ public ERXRouteController(WORequest request) { super(request); _shouldDisposeEditingContext = true; ERXRouteController._registerControllerForRequest(this, request); } /** * Includes the key in the given filter if isKeyPathRequested returns true. * * @param key * the key to lookup * @param filter * the filter to include into * @return the nested filter (or null if the key was not requested) */ protected ERXKeyFilter includeOptional(ERXKey<?> key, ERXKeyFilter filter) { if (isKeyPathRequested(key)) { return filter.include(key); } return ERXKeyFilter.filterWithNone(); // prevent NPE's -- just return an unrooted filter } /** * Returns whether or not the prefetchingKeyPaths option includes the given keypath (meaning, the client requested * to include the given keypath). * * @param key * the ERXKey to check on * @return true if the keyPath is in the prefetchingKeyPaths option */ protected boolean isKeyPathRequested(ERXKey<?> key) { return isKeyPathRequested(key.key()); } /** * Returns whether or not the prefetchingKeyPaths option includes the given keypath (meaning, the client requested * to include the given keypath). * * @param keyPath * the keyPath to check on * @return true if the keyPath is in the prefetchingKeyPaths option */ protected boolean isKeyPathRequested(String keyPath) { if (_prefetchingKeyPaths == null) { NSMutableSet<String> prefetchingKeyPaths = new NSMutableSet<>(); NSKeyValueCoding options = options(); if (options != null) { String prefetchingKeyPathsStr = (String) options.valueForKey("prefetchingKeyPaths"); if (prefetchingKeyPathsStr != null) { for (String prefetchingKeyPath : prefetchingKeyPathsStr.split(",")) { prefetchingKeyPaths.addObject(prefetchingKeyPath); } } } _prefetchingKeyPaths = prefetchingKeyPaths; } return _prefetchingKeyPaths.containsObject(keyPath); } /** * Sets the options for this controller. * * @param options * options for this controller */ public void setOptions(NSKeyValueCoding options) { _options = options; } /** * Returns the options for this controller. Options are an abstraction on request form values. * * @return the options for this controller (default to be ERXRequestFormValues) */ public NSKeyValueCoding options() { if (_options == null) { _options = new ERXRequestFormValues(request()); } return _options; } /** * WODirectAction doesn't expose API for setting the context, which can be useful for passing data between controller. * * @param context the new context */ public void _setContext(WOContext context) { try { Field contextField = WOAction.class.getDeclaredField("_context"); contextField.setAccessible(true); contextField.set(this, context); } catch (Throwable t) { throw NSForwardException._runtimeExceptionForThrowable(t); } } /** * Sets the request handler that processed this route. * * @param requestHandler * the request handler that processed this route */ public void _setRequestHandler(ERXRouteRequestHandler requestHandler) { _requestHandler = requestHandler; } /** * Returns the request handler that processed this route. * * @return the request handler that processed this route */ public ERXRouteRequestHandler requestHandler() { return _requestHandler; } /** * Override to provide custom security checks. It is not necessary to call super on this method. * * @throws SecurityException * if the security check fails */ protected void checkAccess() throws SecurityException { } public void _setEditingContent(EOEditingContext ec) { _editingContext = ec; } /** * The controller maintains an editing context for the duration of the request. The first time you call this method, * you will get a new EOEditingContext. Subsequent calls will return the same instance. This makes it a little more * convenient when you're using update, create, etc methods. * * @return an EOEditingContext */ public EOEditingContext editingContext() { if (_editingContext == null) { ERXRestTransactionRequestAdaptor transactionAdaptor = ERXRestTransactionRequestAdaptor.defaultAdaptor(); if (transactionAdaptor.transactionsEnabled() && transactionAdaptor.isExecutingTransaction(context(), request())) { _editingContext = newEditingContext(transactionAdaptor.executingTransaction(context(), request()).editingContext()); } else { _editingContext = newEditingContext(); } } return _editingContext; } /** * Creates a new editing context. * * @return a new editing context */ protected EOEditingContext newEditingContext() { return ERXEC.newEditingContext(); } /** * Creates a new editing context with a parent object store. * * @param objectStore the parent object store * @return a new editing context */ protected EOEditingContext newEditingContext(EOObjectStore objectStore) { return ERXEC.newEditingContext(objectStore); } /** * Sets the route that is associated with this request. This is typically only set by the request handler. * * @param route * the route that is associated with this controller */ public void _setRoute(ERXRoute route) { _route = route; } /** * Returns the route associated with this request. * * @return the route associated with this request */ public ERXRoute route() { return _route; } /** * Sets the unprocessed keys from the route. * * @param routeKeys * the parsed keys from the route */ public void _setRouteKeys(NSDictionary<ERXRoute.Key, String> routeKeys) { _routeKeys = routeKeys; if (_routeKeys != routeKeys) { _objects = null; } } /** * Returns the unprocessed keys from the route (the values are the original value from the URL). * * @return the unprocessed keys from the route */ public NSDictionary<ERXRoute.Key, String> routeKeys() { return _routeKeys; } /** * Returns the unprocessed value from the route with the given key name. * * @param key * the key name to lookup * @return the unprocessed value from the route with the given key name */ public String routeStringForKey(String key) { return _routeKeys.objectForKey(new ERXRoute.Key(key)); } /** * Returns whether or not there is a route key with the given name. * * @param key * the key name to lookup * @return whether or not there is a route key with the given name */ public boolean containsRouteKey(String key) { return _routeKeys.containsKey(new ERXRoute.Key(key)); } /** * Returns the processed object from the route keys with the given name. For instance, if your route specifies that * you have a {person:Person}, routeObjectForKey("person") will return a Person object. * * @param key * the key name to lookup * @return the processed object from the route keys with the given name */ @SuppressWarnings("unchecked") public <T> T routeObjectForKey(String key) { return (T)routeObjects().objectForKey(new ERXRoute.Key(key)); } /** * Sets the processed objects for the current route. For instance, if your route specifies that you have a * {person:Person}, this dictionary should contain a mapping from that route key to a person instance. * * @param objects the route objects to override */ public void _setRouteObjects(NSDictionary<ERXRoute.Key, Object> objects) { _objects = objects; } /** * Returns all the processed objects from the route keys. For instance, if your route specifies that you have a * {person:Person}, routeObjectForKey("person") will return a Person object. * * @return the processed objects from the route keys */ public NSDictionary<ERXRoute.Key, Object> routeObjects() { if (_objects == null) { _objects = ERXRoute.keysWithObjects(_routeKeys, restContext()); } return _objects; } /** * Returns all the processed objects from the route keys. For instance, if your route specifies that you have a * {person:Person}, routeObjectForKey("person") will return a Person object. This method does NOT cache the results. * * @param restContext the delegate to fetch with * @return the processed objects from the route keys */ public NSDictionary<ERXRoute.Key, Object> routeObjects(ERXRestContext restContext) { if (_route != null) { _objects = ERXRoute.keysWithObjects(_routeKeys, restContext); } return _objects; } /** * Returns the default format to use if no other format is found, or if the requested format is invalid. * * @return the default format to use if no other format is found, or if the requested format is invalid */ protected ERXRestFormat defaultFormat() { String defaultFormatName = ERXProperties.stringForKeyWithDefault("ERXRest.defaultFormat", ERXRestFormat.xml().name()); return ERXRestFormat.formatNamed(defaultFormatName); } /** * Sets the format that will be used by this route controller. * * @param format the format to be used by this route controller */ public void _setFormat(ERXRestFormat format) { _format = format; } /** * Returns the format that the user requested (usually based on the request file extension). * * @return the format that the user requested */ public ERXRestFormat format() { ERXRestFormat format = _format; if (format == null) { String type = null; NSDictionary<String, Object> userInfo = request().userInfo(); if (userInfo != null) { type = (String) request().userInfo().objectForKey(ERXRouteRequestHandler.TypeKey); } /* * To trap things like this: * Content-Type: application/json * JBoss's RestEasy use this header */ if (type == null) { String contentType = request().headerForKey("Content-Type"); if (contentType != null) { String[] types = contentType.split("/"); if (types.length == 2) { type = types[1]; String[] charsets = type.split(";"); if (charsets.length >0) { type = charsets[0]; } } } } if (type == null) { format = defaultFormat(); } else { format = formatNamed(type); } } return format; } /** * Returns the format to use for the given type (see ERXRestFormat constants). * * @param type the type of format to use * @return the corresponding format */ protected ERXRestFormat formatNamed(String type) { return ERXRestFormat.formatNamed(type); } /** * Creates a new rest context for the controller. * * @return a new rest context for the controller */ protected ERXRestContext createRestContext() { return new ERXRestContext(editingContext()); } /** * Returns the cached rest context for this controller. If a rest context doesn't yet * exist, this calls {{@link #createRestContext()} to create a new instance. * * @return the rest context for this controller */ public ERXRestContext restContext() { if (_restContext == null) { _restContext = createRestContext(); } return _restContext; } /** * Sets the rest context for this controller. * * @param restContext the rest context for this controller */ public void setRestContext(ERXRestContext restContext) { _restContext = restContext; } /** * Sets the request content that this controller will use for processing. * @param format the requested format * @param requestContent the content of the incoming request */ public void _setRequestContent(ERXRestFormat format, String requestContent) { _setFormat(format); _setRequestContent(requestContent); } /** * Sets the request content that this controller will use for processing -- this requires that a format() is specified. * @param requestContent the content of the incoming request */ public void _setRequestContent(String requestContent) { _requestNode = format().parse(requestContent); } /** * Sets the request node that this controller will use for processing. * @param requestNode the node reprsenting the incoming request */ public void _setRequestNode(ERXRestRequestNode requestNode) { _requestNode = requestNode; } /** * Returns the default format delegate to use for the given format (defaults to format.delegate()). * * @param format the format to lookup * @return the delegate to use for this format */ protected ERXRestFormat.Delegate formatDelegateForFormat(ERXRestFormat format) { return format.delegate(); } /** * Returns the request data in the form of an ERXRestRequestNode (which is a format-independent wrapper around * hierarchical data). * * @return the request data as an ERXRestRequestNode */ public ERXRestRequestNode requestNode() { if (_requestNode == null) { try { ERXRestFormat format = format(); IERXRestParser parser = format.parser(); if (parser == null) { throw new IllegalStateException("There is no parser for the format '" + format.name() + "'."); } _requestNode = parser.parseRestRequest(new ERXWORestRequest(request()), formatDelegateForFormat(format), restContext()); } catch (Throwable t) { throw new RuntimeException("Failed to parse a " + format() + " request.", t); } } return _requestNode; } /** * Returns the object from the request data that is of the routed entity name and is filtered with the given filter. * This will use the delegate returned from this controller's delegate() method. * * @param filter * the filter to apply to the object for the purposes of updating (or null to not update) * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T object(ERXKeyFilter filter) { return (T)object(entityName(), filter, restContext()); } /** * Returns the object from the request data that is of the given entity name and is filtered with the given filter. * This will use the delegate returned from this controller's delegate() method. * * @param entityName * the entity name of the object in the request * @param filter * the filter to apply to the object for the purposes of updating (or null to not update) * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T object(String entityName, ERXKeyFilter filter) { return (T)object(entityName, filter, restContext()); } /** * Returns the object from the request data that is of the routed entity name and is filtered with the given filter. * * @param filter * the filter to apply to the object for the purposes of updating (or null to not update) * @param restContext * the delegate to use * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T object(ERXKeyFilter filter, ERXRestContext restContext) { return (T)requestNode().objectWithFilter(entityName(), filter, restContext); } /** * Returns the object from the request data that is of the given entity name and is filtered with the given filter. * * @param entityName * the entity name of the object in the request * @param filter * the filter to apply to the object for the purposes of updating (or null to not update) * @param restContext * the delegate to use * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T object(String entityName, ERXKeyFilter filter, ERXRestContext restContext) { return (T)requestNode().objectWithFilter(entityName, filter, restContext); } /** * Creates a new object from the request data that is of the routed entity name and is filtered with the given * filter. This will use the delegate returned from this controller's delegate() method. * * @param filter * the filter to apply to the object for the purposes of updating (or null to just create a blank one) * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T create(ERXKeyFilter filter) { return (T)create(entityName(), filter); } /** * Creates a new object from the request data that is of the given entity name and is filtered with the given * filter. This will use the delegate returned from this controller's delegate() method. * * @param entityName * the entity name of the object in the request * @param filter * the filter to apply to the object for the purposes of updating (or null to just create a blank one) * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T create(String entityName, ERXKeyFilter filter) { return (T)create(entityName, filter, restContext()); } /** * Creates a new object from the request data that is of the routed entity name and is filtered with the given * filter. * * @param filter * the filter to apply to the object for the purposes of updating (or null to just create a blank one) * @param restContext * the delegate to use * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T create(ERXKeyFilter filter, ERXRestContext restContext) { return (T)requestNode().createObjectWithFilter(entityName(), filter, restContext); } /** * Creates a new object from the request data that is of the given entity name and is filtered with the given * filter. * * @param entityName * the entity name of the object in the request * @param filter * the filter to apply to the object for the purposes of updating (or null to just create a blank one) * @param restContext * the delegate to use * @return the object from the request data */ @SuppressWarnings("unchecked") public <T> T create(String entityName, ERXKeyFilter filter, ERXRestContext restContext) { return (T)requestNode().createObjectWithFilter(entityName, filter, restContext); } /** * Updates the given object from the request data with the given filter. This will use the delegate returned from * this controller's delegate() method. * * @param obj * the object to update * @param filter * the filter to apply to the object for the purposes of updating (or null to not update) */ public void update(Object obj, ERXKeyFilter filter) { update(obj, filter, restContext()); } /** * Updates the given object from the request data with the given filter. * * @param obj * object to update * @param filter * the filter to apply to the object for the purposes of updating (or null to not update) * @param restContext * delegate to use */ public void update(Object obj, ERXKeyFilter filter, ERXRestContext restContext) { requestNode().updateObjectWithFilter(obj, filter, restContext); } /** * Returns the given string wrapped in a WOResponse. * * @param str * the string to return * @return a WOResponse */ public WOResponse stringResponse(String str) { WOResponse response = WOApplication.application().createResponseInContext(context()); response.appendContentString(str); return response; } /** * Returns the given array as a JSON response. This uses the editing context returned by editingContext(). * * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a JSON WOResponse */ public WOActionResults json(String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.json(), editingContext(), entityName, values, filter); } /** * Returns the given array as a JSON response. * * @param editingContext * the editing context to use * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a JSON WOResponse */ public WOActionResults json(EOEditingContext editingContext, String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.json(), editingContext, entityName, values, filter); } /** * Returns the given array as a JSON response. * * @param entity * the entity type of the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a JSON WOResponse */ public WOActionResults json(EOClassDescription entity, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.json(), entity, values, filter); } /** * Returns the given array as a PList response. This uses the editing context returned by editingContext(). * * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a PList WOResponse */ public WOActionResults plist(String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.plist(), editingContext(), entityName, values, filter); } /** * Returns the given array as a JSON response. * * @param editingContext * the editing context to use * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a JSON WOResponse */ public WOActionResults plist(EOEditingContext editingContext, String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.plist(), editingContext, entityName, values, filter); } /** * Returns the given array as a JSON response. * * @param entity * the entity type of the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a JSON WOResponse */ public WOActionResults plist(EOClassDescription entity, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.plist(), entity, values, filter); } /** * Returns the given array as an XML response. This uses the editing context returned by editingContext(). * * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return an XML WOResponse */ public WOActionResults xml(String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.xml(), editingContext(), entityName, values, filter); } /** * Returns the given array as an XML response. * * @param editingContext * the editing context to use * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return an XML WOResponse */ public WOActionResults xml(EOEditingContext editingContext, String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.xml(), editingContext, entityName, values, filter); } /** * Returns the given array as an XML response. * * @param entity * the entity type of the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return an XML WOResponse */ public WOActionResults xml(EOClassDescription entity, NSArray<?> values, ERXKeyFilter filter) { return response(ERXRestFormat.xml(), entity, values, filter); } /** * Returns whether or not headers can be added to the given action results. * * @param results the results to test * @return whether or not headers can be added to the given action results */ protected boolean _canSetHeaderForActionResults(WOActionResults results) { return results instanceof WOResponse || results instanceof ERXRouteResults; } /** * Attempt to set the header for the given results object. * * @param value the value * @param key the key * @param results the results object */ protected void _setHeaderForActionResults(String value, String key, WOActionResults results) { if (results instanceof WOResponse) { ((WOResponse)results).setHeader(value, key); } else if (results instanceof ERXRouteResults) { ((ERXRouteResults)results).setHeaderForKey(value, key); } else { log.info("Unable to set a header on an action results of type '{}'.", results.getClass().getName()); } } /** * Returns the results of the rest fetch spec as an response in the format returned from the format() method. * This uses the editing context returned by editingContext(). * * @param fetchSpec * the rest fetch specification to execute * @param filter * the filter to apply to the objects * @return a WOResponse of the format returned from the format() method */ public WOActionResults response(ERXRestFetchSpecification<?> fetchSpec, ERXKeyFilter filter) { WOActionResults results; if (fetchSpec == null) { // MS: you probably meant to call response(Object, filter) in this case -- just proxy through results = response(format(), null, filter); } else { ERXRestFetchSpecification.Results<?> fetchResults = fetchSpec.results(editingContext(), options()); results = response(format(), editingContext(), fetchSpec.entityName(), fetchResults.objects(), filter); if (fetchResults.batchSize() > 0 && options().valueForKey("Range") != null && _canSetHeaderForActionResults(results)) { String contentRangeValue = "items " + fetchResults.startIndex() + "-" + (fetchResults.startIndex() + fetchResults.batchSize() - 1) + "/" + fetchResults.totalCount(); _setHeaderForActionResults(contentRangeValue, "Content-Range", results); } } return results; } /** * Returns the given array as an response in the format returned from the format() method. This uses the editing * context returned by editingContext(). * * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a WOResponse of the format returned from the format() method */ public WOActionResults response(String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(format(), editingContext(), entityName, values, filter); } /** * Returns the given array as an response in the format returned from the format() method. * * @param editingContext * the editing context to use * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a WOResponse of the format returned from the format() method */ public WOActionResults response(EOEditingContext editingContext, String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(format(), editingContext, entityName, values, filter); } /** * Returns the given array as an response in the format returned from the format() method. * * @param entity * the entity type of the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a WOResponse of the format returned from the format() method */ public WOActionResults response(EOClassDescription entity, NSArray<?> values, ERXKeyFilter filter) { return response(format(), entity, values, filter); } /** * Returns the given array as a response in the given format. * * @param format * the format to use * @param entityName * the name of the entity type of the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a WOResponse in the given format */ public WOActionResults response(ERXRestFormat format, String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(format, editingContext(), entityName, values, filter); } /** * Returns the given array as a response in the given format. * * @param format * the format to use * @param editingContext * the editing context to use * @param entityName * the name of the entities in the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a WOResponse in the given format */ public WOActionResults response(ERXRestFormat format, EOEditingContext editingContext, String entityName, NSArray<?> values, ERXKeyFilter filter) { return response(format, ERXRestClassDescriptionFactory.classDescriptionForEntityName(entityName), values, filter); } /** * Returns the given array as a response in the given format. * * @param format * the format to use * @param entity * the entity type of the array * @param values * the values in the array * @param filter * the filter to apply to the objects * @return a WOResponse in the given format */ public WOActionResults response(ERXRestFormat format, EOClassDescription entity, NSArray<?> values, ERXKeyFilter filter) { ERXRestRequestNode responseNode; try { responseNode = ERXRestRequestNode.requestNodeWithObjectAndFilter(entity, values, filter, restContext()); } catch (ObjectNotAvailableException e) { return errorResponse(e, WOMessage.HTTP_STATUS_NOT_FOUND); } catch (SecurityException e) { return errorResponse(e, WOMessage.HTTP_STATUS_FORBIDDEN); } catch (Throwable t) { return errorResponse(t, WOMessage.HTTP_STATUS_INTERNAL_ERROR); } return response(format, responseNode); } /** * Returns the given ERXRestRequestNode as a response in the given format. * * @param format * the format to use * @param responseNode * the request node to render * @return a WOResponse in the given format */ public WOActionResults response(ERXRestFormat format, ERXRestRequestNode responseNode) { ERXRouteResults results = new ERXRouteResults(context(), restContext(), format, responseNode); return results; } /** * Returns the given object as a JSON response. * * @param value * the value to return * @param filter * the filter to apply * @return a WOResponse in JSON format */ public WOActionResults json(Object value, ERXKeyFilter filter) { return response(ERXRestFormat.json(), value, filter); } /** * Returns the given object as a PList response. * * @param value * the value to return * @param filter * the filter to apply * @return a WOResponse in PList format */ public WOActionResults plist(Object value, ERXKeyFilter filter) { return response(ERXRestFormat.plist(), value, filter); } /** * Returns the given object as an XML response. * * @param value * the value to return * @param filter * the filter to apply * @return a WOResponse in XML format */ public WOActionResults xml(Object value, ERXKeyFilter filter) { return response(ERXRestFormat.xml(), value, filter); } /** * Returns the given object as a response in the format returned from the format() method. * * @param value * the value to return * @param filter * the filter to apply * @return a WOResponse in the format returned from the format() method. */ public WOActionResults response(Object value, ERXKeyFilter filter) { return response(format(), value, filter); } /** * Returns the given object as a WOResponse in the given format. * * @param format * the format to use * @param value * the value to return * @param filter * the filter to apply * @return a WOResponse in the given format */ public WOActionResults response(ERXRestFormat format, Object value, ERXKeyFilter filter) { ERXRestRequestNode responseNode; try { responseNode = ERXRestRequestNode.requestNodeWithObjectAndFilter(value, filter, restContext()); } catch (ObjectNotAvailableException e) { return errorResponse(e, WOMessage.HTTP_STATUS_NOT_FOUND); } catch (SecurityException e) { return errorResponse(e, WOMessage.HTTP_STATUS_FORBIDDEN); } catch (Throwable t) { return errorResponse(t, WOMessage.HTTP_STATUS_INTERNAL_ERROR); } return response(format, responseNode); } /** * Returns an response with the given HTTP status and without any body content. * Useful to return HTTP codes like 410 (Gone) or 304 (Not Modified) * @param status * the HTTP status code * @return an error WOResponse */ public WOActionResults response(int status) { WOResponse response = WOApplication.application().createResponseInContext(context()); response.setStatus(status); return response; } /** * Returns an error response with the given HTTP status. * * @param t * the exception * @param status * the HTTP status code * @return an error WOResponse */ public WOActionResults errorResponse(Throwable t, int status) { String errorMessage = ERXLocalizer.defaultLocalizer().localizedStringForKey("ERXRest." + entityName() + ".errorMessage." + status); if (errorMessage == null) { errorMessage = ERXLocalizer.defaultLocalizer().localizedStringForKey("ERXRest.errorMessage." + status); if (errorMessage == null) { errorMessage = ERXExceptionUtilities.toParagraph(t, false); } } String str = format().toString(errorMessage, null, null); WOResponse response = stringResponse(str); response.setStatus(status); if (format().equals(ERXRestFormat.json())) { response.setHeader("application/json", "Content-Type"); } else if (format().equals(ERXRestFormat.xml())) { response.setHeader("text/xml", "Content-Type"); } else if (format().equals(ERXRestFormat.plist())) { response.setHeader("text/plist", "Content-Type"); } else if (format().equals(ERXRestFormat.bplist())) { response.setHeader("application/x-plist", "Content-Type"); } else { response.setHeader("application/json", "Content-Type"); } log.error("Request failed: {}", request().uri(), t); return response; } /** * Returns an error response with the given HTTP status. * * @param errorMessage * the error message * @param status * the HTTP status code * @return an error WOResponse */ public WOActionResults errorResponse(String errorMessage, int status) { String formattedErrorMessage = format().toString(errorMessage, null, null); WOResponse response = stringResponse(formattedErrorMessage); response.setStatus(status); log.error("Request failed: {}, {}", request().uri(), errorMessage); return response; } /** * Returns an error response with the given HTTP status and without any body content * @param status * the HTTP status code * @return an error WOResponse */ public WOActionResults errorResponse(int status) { WOResponse response = WOApplication.application().createResponseInContext(context()); response.setStatus(status); log.error("Request failed: {}, {}", request().uri(), status); return response; } /** * Returns the response from a HEAD call to this controller. * * @return a head response */ public WOActionResults headAction() { WOResponse response = WOApplication.application().createResponseInContext(context()); format().writer().appendHeadersToResponse(null, new ERXWORestResponse(response), restContext()); return response; } /** * Enumerates the route keys, looks for @ERXRouteParameter annotated methods, and sets the value of the routeKey * with the corresponding method if it exists. * * @param results * the results to apply route parameter to */ protected void _takeRouteParametersFromRequest(WOActionResults results) { Class<?> resultsClass = results.getClass(); for (ERXRoute.Key key : _routeKeys.allKeys()) { ERXRoute.RouteParameterMethod routeParameterMethod = key._routeParameterMethodForClass(resultsClass); String keyName = key.key(); if (routeParameterMethod == null) { // MS: because we lowercase SPPerson into spPerson, the default capitalization would be SpPerson. // We want to do a first pass where we check for entities that equalsIgnoreCase match the // keyName and guess that as the capitalization first. If that fails, THEN we fall back to a // simple capitalization. String capitalizedKeyName = ERXRestClassDescriptionFactory._guessMismatchedCaseEntityName(keyName); if (capitalizedKeyName == null) { capitalizedKeyName = ERXStringUtilities.capitalize(keyName); } String setMethodName = "set" + capitalizedKeyName; Method matchingMethod = null; Method[] possibleMethods = resultsClass.getMethods(); for (Method possibleMethod : possibleMethods) { ERXRouteParameter routeParameter = possibleMethod.getAnnotation(ERXRouteParameter.class); // IK : in SnapshotExplorer the er.snapshotexplorer.components.pages.setEOModelGroup get never called because // setMethodName will be "setEoModelGroup" and with the equals compare never hits. // Now with changeds to equalsIgnoreCase it works again. Also I belive there will be no Problme except somebody // has two Methods with the same Name but different capitalizations. if (routeParameter != null && (keyName.equals(routeParameter.value()) || possibleMethod.getName().equalsIgnoreCase(setMethodName))) { matchingMethod = possibleMethod; break; } } routeParameterMethod = new ERXRoute.RouteParameterMethod(matchingMethod); key._setRouteParameterMethodForClass(routeParameterMethod, resultsClass); } if (routeParameterMethod.hasMethod()) { try { if (routeParameterMethod.isStringParameter()) { routeParameterMethod.method().invoke(results, routeStringForKey(keyName)); } else { Object routeObject = routeObjectForKey(keyName); if (routeObject instanceof EOEnterpriseObject && ((EOEnterpriseObject)routeObject).editingContext() == _editingContext) { _shouldDisposeEditingContext = false; } routeParameterMethod.method().invoke(results, routeObject); } } catch (Throwable t) { throw NSForwardException._runtimeExceptionForThrowable(t); } } } } /** * If this method returns true, all HTML format requests will be automatically routed to the corresponding * IERXRouteComponent implementation based on the name returned by pageNameForAction(String). * * @return true if HTML format requests should be automatically routed to the corresponding page component */ protected boolean isAutomaticHtmlRoutingEnabled() { return false; } /** * If automatic html routing is enabled and there is no page component found that matches the current route, * should that result in a 404? * * @return whether or not a missing page is a failure */ protected boolean shouldFailOnMissingHtmlPage() { return false; } /** * Sets the entity name for this controller. * * @param entityName this controller's entity name */ public void _setEntityName(String entityName) { _entityName = entityName; } /** * Returns the name of the entity that this controller is currently handling. The default implementation retrieves * the entity name from the ERXRoute. * * @return the entity name for the current route */ protected String entityName() { String entityName = _entityName; if (entityName == null) { ERXRoute route = route(); if (route != null) { entityName = route.entityName(); } } if (entityName == null) { throw new IllegalStateException("Unable to determine the entity name for the controller '" + getClass().getSimpleName() + "'. Please override entityName()."); } return entityName; } /** * Returns the name of the page component for this entity and the given action. The default implementation of this * returns entityName + Action + Page ("PersonEditPage", "PersonViewPage", etc). * * @param actionName * the name of the action * @return the name of the page component for this action */ protected String pageNameForAction(String actionName) { return entityName() + ERXStringUtilities.capitalize(actionName) + "Page"; } /** * Called when no standard action method can be found to handle the requested route. The default * implementation just throws an exception. * * @param actionName the unknown action name * @return WOActionResults */ protected WOActionResults performUnknownAction(String actionName) throws Exception { boolean isStrictMode = ERXProperties.booleanForKeyWithDefault("ERXRest.strictMode", true); if (isStrictMode) { throw new ERXNotAllowedException(); } else { throw new FileNotFoundException("There is no action named '" + actionName + "Action' on '" + getClass().getSimpleName() + "'."); } } @Override public WOActionResults performActionNamed(String actionName) { return performActionNamed(actionName, false); } /** * Returns the response node generated from performing the action with the given name. * * @param actionName the name of the action to perform * @return the response node */ public ERXRestRequestNode responseNodeForActionNamed(String actionName) { String contentString = performActionNamed(actionName, true).generateResponse().contentString(); return format().parse(contentString); } /** * Returns the response content generated from performing the action with the given name. * * @param actionName the name of the action to perform * @return the response content */ public String responseContentForActionNamed(String actionName) { return performActionNamed(actionName, true).generateResponse().contentString(); } /** * Performs the given action, optionally throwing exceptions instead of converting to http response codes. * * @param actionName the name of the action to perform * @param throwExceptions whether or not to throw exceptions * @return the action results * @throws RuntimeException if a failure occurs */ public WOActionResults performActionNamed(String actionName, boolean throwExceptions) throws RuntimeException { WOActionResults results = null; try { ERXRestTransactionRequestAdaptor transactionAdaptor = ERXRestTransactionRequestAdaptor.defaultAdaptor(); if (transactionAdaptor.transactionsEnabled() && !transactionAdaptor.isExecutingTransaction(context(), request())) { if (!transactionAdaptor.willHandleRequest(context(), request())) { if (transactionAdaptor.didHandleRequest(context(), request())) { results = stringResponse("Transaction request enqueued."); } else { results = stringResponse("Transaction executed."); } } } if (results == null) { checkAccess(); } if (results == null && isAutomaticHtmlRoutingEnabled() && format() == ERXRestFormat.html()) { results = performHtmlActionNamed(actionName); } if (results == null) { results = performRouteActionNamed(actionName); } if (results == null) { results = response(null, ERXKeyFilter.filterWithAttributes()); } else if (results instanceof IERXRouteComponent) { _takeRouteParametersFromRequest(results); } } catch (Throwable t) { if (throwExceptions) { throw NSForwardException._runtimeExceptionForThrowable(t); } results = performActionNamedWithError(actionName, t); } results = processActionResults(results); return results; } /** * If automatic HTML routing is enabled and this request used an HTML format, this method is called * to dispatch the HTML action. * * @param actionName the name of the HTML action * @return the results of the action * @throws Exception if anything fails */ protected WOActionResults performHtmlActionNamed(String actionName) throws Exception { WOActionResults results = null; String pageName = pageNameForAction(actionName); if (_NSUtilities.classWithName(pageName) != null) { try { results = pageWithName(pageName); if (!(results instanceof IERXRouteComponent)) { log.error("{} does not implement IERXRouteComponent, so it will be ignored.", pageName); results = null; } } catch (WOPageNotFoundException e) { log.info("{} does not exist, falling back to route controller.", pageName); results = null; } } else { log.info("{} does not exist, falling back to route controller.", pageName); } if (results == null && shouldFailOnMissingHtmlPage()) { results = performUnknownAction(actionName); } return results; } /** * If this request is for a normal route action, this method is called to dispatch it. * * @param actionName the name of the action to perform * @return the results of the action * @throws Exception if anything fails */ protected WOActionResults performRouteActionNamed(String actionName) throws Exception { WOActionResults results = null; String actionMethodName = actionName + WODirectAction.actionText; Method actionMethod = _methodForAction(actionMethodName, ""); if (actionMethod == null) { actionMethod = _methodForAction(actionName, ""); if (actionMethod == null || (actionMethod.getAnnotation(Path.class) == null && actionMethod.getAnnotation(Paths.class) == null)) { actionMethod = null; } } if (actionMethod == null || actionMethod.getParameterTypes().length > 0) { actionMethod = null; int bestMatchParameterCount = 0; List<Annotation> bestMatchAnnotations = null; for (Method method : getClass().getDeclaredMethods()) { String methodName = method.getName(); boolean nameMatches = methodName.equals(actionMethodName); if (!nameMatches && methodName.equals(actionName) && (method.getAnnotation(Path.class) != null || method.getAnnotation(Paths.class) != null)) { nameMatches = true; } if (nameMatches) { int parameterCount = 0; List<Annotation> params = new LinkedList<>(); for (Annotation[] parameterAnnotations : method.getParameterAnnotations()) { for (Annotation parameterAnnotation : parameterAnnotations) { if (parameterAnnotation instanceof PathParam || parameterAnnotation instanceof QueryParam || parameterAnnotation instanceof CookieParam || parameterAnnotation instanceof HeaderParam) { params.add(parameterAnnotation); parameterCount ++; } else { parameterCount = -1; break; } } if (parameterCount == -1) { break; } } if (parameterCount > bestMatchParameterCount) { actionMethod = method; bestMatchParameterCount = parameterCount; bestMatchAnnotations = params; } } } if (actionMethod == null) { results = performUnknownAction(actionName); } else if (bestMatchParameterCount == 0) { results = performActionWithArguments(actionMethod, _NSUtilities._NoObjectArray); } else { results = performActionWithAnnotations(actionMethod, bestMatchAnnotations); } } else { results = performActionWithArguments(actionMethod, _NSUtilities._NoObjectArray); } return results; } /** * Called when performRouteAction dispatches a method that uses parameter annotations. * * @param actionMethod the action method to dispatch * @param parameterAnnotations the list of annotations * @return the results of the action * @throws Exception if anything fails */ protected WOActionResults performActionWithAnnotations(Method actionMethod, List<Annotation> parameterAnnotations) throws Exception { Class<?>[] parameterTypes = actionMethod.getParameterTypes(); Object[] params = new Object[parameterAnnotations.size()]; for (int paramNum = 0; paramNum < params.length; paramNum ++) { Annotation param = parameterAnnotations.get(paramNum); if (param instanceof PathParam) { params[paramNum] = routeObjectForKey(((PathParam)param).value()); } else if (param instanceof QueryParam) { String value = request().stringFormValueForKey(((QueryParam)param).value()); params[paramNum] = ERXRestUtils.coerceValueToTypeNamed(value, parameterTypes[paramNum].getName(), restContext(), true); } else if (param instanceof CookieParam) { String value = request().cookieValueForKey(((CookieParam)param).value()); params[paramNum] = ERXRestUtils.coerceValueToTypeNamed(value, parameterTypes[paramNum].getName(), restContext(), true); } else if (param instanceof HeaderParam) { String value = request().headerForKey(((HeaderParam)param).value()); params[paramNum] = ERXRestUtils.coerceValueToTypeNamed(value, parameterTypes[paramNum].getName(), restContext(), true); } else { throw new IllegalArgumentException("Unknown parameter #" + paramNum + " of " + actionMethod.getName() + "."); } } return performActionWithArguments(actionMethod, params); } /** * Called when an action method is dispatched by performRouteAction in any form. * * @param actionMethod the method to invoke * @param args the arguments to pass to the method * @return the results of the method * @throws Exception if anything fails */ protected WOActionResults performActionWithArguments(Method actionMethod, Object... args) throws Exception { return (WOActionResults)actionMethod.invoke(this, args); } /** * Called when performing an action fails, giving a chance to return an appropriate error result. * * @param actionName the name of the action that attempted to perform * @param t the error that occurred * @return an appropriate error result */ protected WOActionResults performActionNamedWithError(String actionName, Throwable t) { WOActionResults results = null; Throwable meaningfulThrowble = ERXExceptionUtilities.getMeaningfulThrowable(t); boolean isStrictMode = ERXProperties.booleanForKeyWithDefault("ERXRest.strictMode", true); if (meaningfulThrowble instanceof ObjectNotAvailableException || meaningfulThrowble instanceof FileNotFoundException || meaningfulThrowble instanceof NoSuchElementException) { results = errorResponse(meaningfulThrowble, ERXHttpStatusCodes.NOT_FOUND); } else if (meaningfulThrowble instanceof ERXBasicAuthenticationException) { WOResponse response = (WOResponse) errorResponse(meaningfulThrowble, ERXHttpStatusCodes.UNAUTHORIZED); response.setHeader("Basic realm=\"" + ((ERXBasicAuthenticationException) meaningfulThrowble).realm() + "\"", "WWW-Authenticate"); results = response; } else if (meaningfulThrowble instanceof SecurityException) { results = errorResponse(meaningfulThrowble, ERXHttpStatusCodes.FORBIDDEN); } else if (meaningfulThrowble instanceof ERXNotAllowedException) { results = errorResponse(ERXHttpStatusCodes.METHOD_NOT_ALLOWED); } else if ((isStrictMode) && (meaningfulThrowble instanceof ERXValidationException || meaningfulThrowble instanceof NSValidation.ValidationException)) { results = errorResponse(meaningfulThrowble, ERXHttpStatusCodes.BAD_REQUEST); } else { results = errorResponse(meaningfulThrowble,ERXHttpStatusCodes.INTERNAL_ERROR); } // MS: Should we jam the exception in the response userInfo so the transaction adaptor can rethrow the real exception? return results; } /** * Before returning the action results, this method is called to perform any last minute processing. * * @param results */ protected WOActionResults processActionResults(WOActionResults results) { WOContext context = context(); WOSession session = context._session(); // MS: This is sketchy -- should this be done in the request handler after we generate the response? if (results instanceof WOResponse) { WOResponse response = (WOResponse)results; if (session != null && session.storesIDsInCookies()) { session._appendCookieToResponse(response); } } if (_canSetHeaderForActionResults(results)) { String allowOrigin = accessControlAllowOrigin(); if (allowOrigin != null) { _setHeaderForActionResults(allowOrigin, "Access-Control-Allow-Origin", results); } } WOActionResults processedResults = results; if (allowWindowNameCrossDomainTransport()) { String windowNameCrossDomainTransport = request().stringFormValueForKey("windowname"); if ("true".equals(windowNameCrossDomainTransport)) { WOResponse response = results.generateResponse(); String content = response.contentString(); if (content != null) { content = content.replaceAll("\n", ""); content = ERXStringUtilities.escapeJavascriptApostrophes(content); } response.setContent("<html><script type=\"text/javascript\">window.name='" + content + "';</script></html>"); response.setHeader("text/html", "Content-Type"); processedResults = response; } } if (allowJSONP()) { if (format().equals(ERXRestFormat.json())) { String callbackMethodName = request().stringFormValueForKey("callback"); if (callbackMethodName != null) { WOResponse response = results.generateResponse(); String content = response.contentString(); if (content != null) { content = content.replaceAll("\n", ""); content = ERXStringUtilities.escapeJavascriptApostrophes(content); } response.setContent(callbackMethodName + "(" + content + ");"); response.setHeader("text/javascript", "Content-Type"); processedResults = response; } } } return processedResults; } /** * Returns whether or not the window.name cross-domain transport is allowed. * * @return whether or not the window.name cross-domain transport is allowed */ protected boolean allowWindowNameCrossDomainTransport() { return ERXProperties.booleanForKeyWithDefault("ERXRest.allowWindowNameCrossDomainTransport", false); } /** * Returns whether or not JSONP (JSON with Padding) is allowed. * * @return whether or not JSONP (JSON with Padding) is allowed */ protected boolean allowJSONP() { return ERXProperties.booleanForKeyWithDefault("ERXRest.allowJSONP", false); } /** * Returns the allowed origin for cross-site requests. Set the property ERXRest.accessControlAllowOrigin=* to enable all origins. * * @return the allowed origin for cross-site requests */ protected String accessControlAllowOrigin() { return ERXProperties.stringForKeyWithDefault("ERXRest.accessControlAllowOrigin", null); } /** * Returns the allowed request methods given the requested method. Set the property ERXRest.accessControlAllowRequestMethods to override * the default of returning OPTIONS,GET,HEAD,POST,PUT,DELETE,TRACE,CONNECT. * * @param requestMethod the requested method * @return the array of allowed request methods */ protected NSArray<String> accessControlAllowRequestMethods(String requestMethod) { String accessControlAllowRequestMethodsStr = ERXProperties.stringForKeyWithDefault("ERXRest.accessControlAllowRequestMethods", "OPTIONS,GET,HEAD,POST,PUT,DELETE,TRACE,CONNECT"); if (accessControlAllowRequestMethodsStr == null || accessControlAllowRequestMethodsStr.length() == 0) { accessControlAllowRequestMethodsStr = requestMethod; } NSArray<String> accessControlAllowRequestMethods = null; if (accessControlAllowRequestMethodsStr != null) { accessControlAllowRequestMethods = new NSArray<>(accessControlAllowRequestMethodsStr.split(",")); } return accessControlAllowRequestMethods; } /** * Returns the allowed request headers given the requested headers.Set the property ERXRest.accessControlAllowRequestHeaders to override * the default of just returning the requested headers. * * @param requestHeaders the requested headers * @return the array of allowed request headers */ protected NSArray<String> accessControlAllowRequestHeaders(NSArray<String> requestHeaders) { String requestHeadersStr = requestHeaders == null ? null : requestHeaders.componentsJoinedByString(","); String accessControlAllowRequestHeadersStr = ERXProperties.stringForKeyWithDefault("ERXRest.accessControlAllowRequestHeaders", requestHeadersStr); NSArray<String> accessControlAllowRequestHeaders = null; if (accessControlAllowRequestHeadersStr != null) { accessControlAllowRequestHeaders = new NSArray<>(accessControlAllowRequestHeadersStr.split(",")); } return accessControlAllowRequestHeaders; } /** * Returns the maximum age in seconds for the preflight options cache. * * @return the maximum age for the preflight options cache */ protected long accessControlMaxAage() { return ERXProperties.longForKeyWithDefault("ERXRest.accessControlMaxAge", 1728000); } /** * A default options action that implements access control policy. * * @return the response */ public WOActionResults optionsAction() throws Throwable { ERXResponse response = new ERXResponse(); String accessControlAllowOrigin = accessControlAllowOrigin(); if (accessControlAllowOrigin != null) { response.setHeader(accessControlAllowOrigin, "Access-Control-Allow-Origin"); NSArray<String> accessControlAllowRequestMethods = accessControlAllowRequestMethods(request().headerForKey("Access-Control-Request-Method")); if (accessControlAllowRequestMethods != null) { response.setHeader(accessControlAllowRequestMethods.componentsJoinedByString(","), "Access-Control-Allow-Methods"); } String requestHeadersStr = request().headerForKey("Access-Control-Request-Headers"); NSArray<String> requestHeaders = (requestHeadersStr == null) ? null : NSArray.componentsSeparatedByString(requestHeadersStr, ","); NSArray<String> accessControlAllowRequestHeaders = accessControlAllowRequestHeaders(requestHeaders); if (accessControlAllowRequestHeaders != null) { response.setHeader(accessControlAllowRequestHeaders.componentsJoinedByString(","), "Access-Control-Allow-Headers"); } long accessControlMaxAge = accessControlMaxAage(); if (accessControlMaxAge >= 0) { response.setHeader(String.valueOf(accessControlMaxAge), "Access-Control-Max-Age"); } } return response; } /** * Calls pageWithName. * * @param <T> * the type of component to return * @param componentClass * the component class to lookup * @return the created component */ @SuppressWarnings("unchecked") public <T extends WOComponent> T pageWithName(Class<T> componentClass) { return (T) super.pageWithName(componentClass.getName()); } /** * Returns another controller, passing the required state on. * * @param <T> * the type of controller to return * @param entityName * the entity name of the controller to lookup * @return the created controller */ @SuppressWarnings("unchecked") public <T extends ERXRouteController> T controller(String entityName) { return controller((Class<T>) requestHandler().routeControllerClassForEntityNamed(entityName)); } /** * Returns another controller, passing the required state on. * * @param <T> * the type of controller to return * @param controllerClass * the controller class to lookup * @return the created controller */ public <T extends ERXRouteController> T controller(Class<T> controllerClass) { try { T controller = requestHandler().controller(controllerClass, request(), context()); controller._setRoute(_route); controller._setEditingContent(_editingContext); controller._setRouteKeys(_routeKeys); controller._setRouteObjects(_objects); controller.setOptions(_options); return controller; } catch (Exception e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } /** * Disposes any resources the route controller may be holding onto (like its editing context). */ public void dispose() { if (_shouldDisposeEditingContext && _editingContext != null) { if(_editingContext instanceof ERXEC && ((ERXEC) _editingContext).isAutoLocked()) { _editingContext.unlock(); } _editingContext.dispose(); _editingContext = null; } } /** * Returns whether or not this request is for a schema. * * @return whether or not this request is for a schema */ protected boolean isSchemaRequest() { return request().stringFormValueForKey("schema") != null; } /** * Returns the schema response for the current entity with the given filter. * * @param filter the filter to apply * @return the schema response for the current entity with the given filter */ protected WOActionResults schemaResponse(ERXKeyFilter filter) { return schemaResponseForEntityNamed(entityName(), filter); } /** * Returns the schema response for the given entity with the given filter. * * @param entityName the entity name * @param filter the filter to apply * @return the schema response for the given entity with the given filter */ protected WOActionResults schemaResponseForEntityNamed(String entityName, ERXKeyFilter filter) { NSDictionary<String, Object> properties = ERXRestSchema.schemaForEntityNamed(entityName, filter); return response(properties, ERXKeyFilter.filterWithAllRecursive()); } @Override public String toString() { return "[" + getClass().getSimpleName() + ": " + request().uri() + "]"; } private static final String REQUEST_CONTROLLERS_KEY = "ERRest.controllersForRequest"; /** * Registers the given controller with the given request, so it can be later disposed. This can be a * very useful performance optimization for apps that gets a large number of requests. * * @param controller the controller to register * @param request the request to register with */ protected static void _registerControllerForRequest(ERXRouteController controller, WORequest request) { NSMutableArray<ERXRouteController> controllers = _controllersForRequest(request); if (controllers == null) { controllers = new NSMutableArray<>(); if (request != null) { NSMutableDictionary<String, Object> userInfo = ((ERXRequest)request).mutableUserInfo(); userInfo.setObjectForKey(controllers, ERXRouteController.REQUEST_CONTROLLERS_KEY); } } controllers.addObject(controller); } /** * Returns the controllers that have been used on the given request. * * @param request the request */ @SuppressWarnings("unchecked") public static NSMutableArray<ERXRouteController> _controllersForRequest(WORequest request) { NSDictionary<String, Object> userInfo = request != null ? request.userInfo() : null; NSMutableArray<ERXRouteController> controllers = null; if (userInfo != null) { controllers = (NSMutableArray<ERXRouteController>)userInfo.objectForKey(ERXRouteController.REQUEST_CONTROLLERS_KEY); } return controllers; } /** * Disposes all of the controllers that were used on the given request. * * @param request the request */ public static void _disposeControllersForRequest(WORequest request) { NSArray<ERXRouteController> controllers = ERXRouteController._controllersForRequest(request); if (controllers != null) { for (ERXRouteController controller : controllers) { controller.dispose(); } } } }