package er.rest.routes;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOAction;
import com.webobjects.appserver.WOApplication;
import com.webobjects.appserver.WOContext;
import com.webobjects.appserver.WORequest;
import com.webobjects.appserver._private.WODirectActionRequestHandler;
import com.webobjects.eocontrol.EOClassDescription;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation._NSUtilities;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.localization.ERXLocalizer;
import er.rest.ERXRestClassDescriptionFactory;
import er.rest.ERXRestNameRegistry;
import er.rest.IERXRestDelegate;
import er.rest.format.ERXRestFormat;
import er.rest.routes.jsr311.HttpMethod;
import er.rest.routes.jsr311.Path;
import er.rest.routes.jsr311.Paths;
/**
* ERXRouteRequestHandler is the request handler that can process rails-style route mappings and convert them to
* ERXRestController action methods.
*
* in Application:
*
* <pre>
* ERXRouteRequestHandler routeRequestHandler = new ERXRouteRequestHandler();
* routeRequestHandler.addDefaultRoutes(Person.ENTITY_NAME);
* ERXRouteRequestHandler.register(routeRequestHandler);
* </pre>
*
* or
*
* <pre>
* ERXRouteRequestHandler routeRequestHandler = new ERXRouteRequestHandler();
* routeRequestHandler.addRoute(new ERXRoute("/people/{action}", PeopleController.class));
* routeRequestHandler.addRoute(new ERXRoute("/person/{person:Person}", PeopleController.class, "show"));
* ...
* ERXRouteRequestHandler.register(routeRequestHandler);
* </pre>
*
* Note that addDefaultRoutes sets up many routes automatically (not just the 2 that are shown above), and for most
* cases should be your starting point for adding new entities rather than manually adding them.
*
* in PeopleController:
*
* <pre>
* public class PeopleController extends ERXRouteController {
* public PeopleController(WORequest request) {
* super(request);
* }
*
* public Person person() {
* Person person = (Person) routeObjectForKey("person");
* return person;
* }
*
* public ERXKeyFilter showFilter() {
* ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes();
* filter.include(Person.COMPANY).includeAttributes();
* return filter;
* }
*
* public ERXKeyFilter updateFilter() {
* ERXKeyFilter filter = ERXKeyFilter.filterWithAttributes();
* filter.include(Person.COMPANY);
* return filter;
* }
*
* public WOActionResults createAction() {
* Person person = (Person) create(Person.ENTITY_NAME, updateFilter());
* editingContext().saveChanges();
* return response(person, showFilter());
* }
*
* public WOActionResults updateAction() {
* Person person = person();
* update(person, updateFilter());
* editingContext().saveChanges();
* return response(person, showFilter());
* }
*
* public WOActionResults showAction() {
* return response(person(), showFilter());
* }
*
* public WOActionResults indexAction() {
* NSArray<Person> people = Person.fetchPersons(editingContext(), null, Person.LAST_NAME.asc().then(Person.FIRST_NAME.asc()));
* return response(editingContext(), Person.ENTITY_NAME, people, showFilter());
* }
* }
* </pre>
*
* in browser:
*
* <pre>
* http://localhost/cgi-bin/WebObjects/YourApp.woa/ra/people.xml
* http://localhost/cgi-bin/WebObjects/YourApp.woa/ra/people.json
* http://localhost/cgi-bin/WebObjects/YourApp.woa/ra/people.plist
* http://localhost/cgi-bin/WebObjects/YourApp.woa/ra/person/100.json
* http://localhost/cgi-bin/WebObjects/YourApp.woa/ra/person/100/edit.json
* </pre>
*
* @property ERXRest.missingControllerName (default "ERXMissingRouteController") Allow you to specify which controller to use when a route doesn't exist
* @property ERXRest.parseUnknownExtensions (default "true") If set to "false", will return a 404 status code if the format doesn't exist
* @property ERXRest.pluralEntityNames
* @property ERXRest.routeCase
* @property ERXRest.lowercaseEntityNames
*
* @author mschrag
*/
public class ERXRouteRequestHandler extends WODirectActionRequestHandler {
/**
* Header used to override the method of a request. Useful when client code can use only GET and POST methods.
*/
private static final String X_HTTP_METHOD_OVERRIDE_HEADER_KEY = "x-http-method-override";
/**
* A NameFormat that behaves like Rails -- plural entities, plural routes, lowercase underscore names
* (names_like_this).
*/
public static NameFormat RAILS = new NameFormat(true, true, NameFormat.Case.LowercaseUnderscore);
/**
* A NameFormat that behaves like WO -- singular entities, singular routes, camel names (NamesLikeThis).
*/
public static NameFormat WO = new NameFormat(false, false, NameFormat.Case.CamelCase);
/**
* A NameFormat that behaves like WO -- singular entities, singular routes, lowercase camel names (namesLikeThis).
*/
public static NameFormat WO_LOWER = new NameFormat(false, false, NameFormat.Case.LowerCamelCase);
public static NameFormat EMBER = new NameFormat(true, true, NameFormat.Case.LowerCamelCase);
/**
* NameFormat specifies how routes and controller names should be capitalized by default.
*
* @author mschrag
*/
public static class NameFormat {
/**
* An enumerated type specifying the case of your routes.
*
* @author mschrag
*/
public static enum Case {
/**
* CamelCase
*/
CamelCase,
/**
* lowerCamelCase
*/
LowerCamelCase,
/**
* lowercase
*/
Lowercase,
/**
* lowercase_underscore
*/
LowercaseUnderscore
}
private final boolean _pluralControllerName;
private final boolean _pluralRouteName;
private final Case _routeCase;
/**
* Creates a new NameFormat.
*
* @param pluralControllerName
* if true, controller names with be pluralized ("CompaniesController")
* @param pluralRouteName
* if true, routes will be pluralizd ("/Companies.xml")
* @param routeCase
* the case to use for the route name
*/
public NameFormat(boolean pluralControllerName, boolean pluralRouteName, NameFormat.Case routeCase) {
_pluralControllerName = pluralControllerName;
_pluralRouteName = pluralRouteName;
_routeCase = routeCase;
}
/**
* Returns whether or not controller names should be pluralizd.
*
* @return whether or not controller names should be pluralizd
*/
public boolean pluralControllerName() {
return _pluralControllerName;
}
/**
* Returns whether or not routes should be pluralizd.
*
* @return whether or not routes should be pluralizd
*/
public boolean pluralRouteName() {
return _pluralRouteName;
}
/**
* Returns the case to use for routes.
*
* @return whether or not routes should be capitalized
*/
public NameFormat.Case routeCase() {
return _routeCase;
}
/**
* Applies the case transformation to the given string.
*
* @param entityName
* the string to adjust the case of
* @return the case-adjusted string
*/
protected String caseifyEntityNamed(String entityName) {
String formattedStr;
if (_routeCase == NameFormat.Case.CamelCase) {
formattedStr = entityName;
}
else if (_routeCase == NameFormat.Case.LowerCamelCase) {
formattedStr = ERXStringUtilities.uncapitalize(entityName);
}
else if (_routeCase == NameFormat.Case.Lowercase) {
formattedStr = entityName.toLowerCase();
}
else if (_routeCase == NameFormat.Case.LowercaseUnderscore) {
formattedStr = ERXStringUtilities.camelCaseToUnderscore(entityName, true);
}
else {
throw new IllegalArgumentException("Unknown case: " + _routeCase);
}
return formattedStr;
}
/**
* Formats the given entity name based on the rules of this format.
*
* @param entityName
* the entity name to format
* @param pluralizeIfNecessary
* if pluralRouteNames() is true, return the plural form
* @return the formatted entity name
*/
public String formatEntityNamed(String entityName, boolean pluralizeIfNecessary) {
String singularEntityName = caseifyEntityNamed(entityName);
if ((entityName == null) || (entityName.length() == 0)) {
return singularEntityName;
}
String controllerPath;
if (pluralizeIfNecessary && pluralRouteName()) {
controllerPath = ERXLocalizer.englishLocalizer().plurifiedString(singularEntityName, 2);
}
else {
controllerPath = singularEntityName;
}
return controllerPath;
}
}
private static final Logger log = LoggerFactory.getLogger(ERXRouteRequestHandler.class);
public static final String Key = "ra";
public static final String TypeKey = "ERXRouteRequestHandler.type";
public static final String ExtensionKey = "ERXRouteRequestHandler.extension";
public static final String PathKey = "ERXRouteRequestHandler.path";
public static final String RouteKey = "ERXRouteRequestHandler.route";
public static final String KeysKey = "ERXRouteRequestHandler.keys";
private final NameFormat _entityNameFormat;
private final NSMutableArray<ERXRoute> _routes;
private final boolean _parseUnknownExtensions;
/**
* Constructs a new ERXRouteRequestHandler with the default entity name format.
*/
public ERXRouteRequestHandler() {
this(new NameFormat(ERXProperties.booleanForKeyWithDefault("ERXRest.pluralEntityNames", true), ERXProperties.booleanForKeyWithDefault("ERXRest.pluralEntityNames", true), NameFormat.Case.valueOf(ERXProperties.stringForKeyWithDefault("ERXRest.routeCase", ERXProperties.booleanForKeyWithDefault("ERXRest.lowercaseEntityNames", true) ? NameFormat.Case.LowerCamelCase.name() : NameFormat.Case.CamelCase.name()))));
}
/**
* Constructs a new ERXRouteRequestHandler.
*
* @param entityNameFormat
* the format to use for entity names in URLs
*/
public ERXRouteRequestHandler(NameFormat entityNameFormat) {
_entityNameFormat = entityNameFormat;
_routes = new NSMutableArray<>();
_parseUnknownExtensions = ERXProperties.booleanForKeyWithDefault("ERXRest.parseUnknownExtensions", true);
}
/**
* Inserts a route at the beginning of the route list.
*
* @param route
* the route to insert
*/
public void insertRoute(ERXRoute route) {
verifyRoute(route);
_routes.insertObjectAtIndex(route, 0);
}
/**
* Adds a new route to this request handler.
*
* @param route
* the route to add
*/
public void addRoute(ERXRoute route) {
log.debug("adding route {}", route);
verifyRoute(route);
_routes.addObject(route);
}
/**
* Removes the given route from this request handler.
*
* @param route
* the route to remove
*/
public void removeRoute(ERXRoute route) {
_routes.removeObject(route);
}
/**
* Clears any caches that may exist on ERXRoutes (probably only useful to JRebel, to clear the route parameter method cache).
*/
public void _clearCaches() {
for (ERXRoute route : _routes) {
route._clearCaches();
}
}
/**
* Returns the routes for this request handler.
*
* @return the routes for this request handler.
*/
public NSArray<ERXRoute> routes() {
return _routes.immutableClone();
}
/**
* Returns the routes for the given controller class.
*
* @param routeController the controller class
* @return the routes for the given controller class
*/
public NSArray<ERXRoute> routesForControllerClass(Class<? extends ERXRouteController> routeController) {
NSMutableArray<ERXRoute> routes = new NSMutableArray<>();
for (ERXRoute route : _routes) {
if (route.controller() == routeController) {
routes.add(route);
}
}
return routes;
}
/**
* Returns the default route controller class for the given entity name.
*
* @param entityName
* the name of the entity
* @return the corresponding route controller
*/
public Class<? extends ERXRouteController> routeControllerClassForEntityNamed(String entityName) {
String controllerName = entityName + "Controller";
Class<?> controllerClass = _NSUtilities.classWithName(controllerName);
if (controllerClass == null) {
String pluralControllerName = ERXLocalizer.englishLocalizer().plurifiedString(entityName, 2) + "Controller";
controllerClass = _NSUtilities.classWithName(pluralControllerName);
if (controllerClass == null) {
throw new IllegalArgumentException("There is no controller named '" + controllerName + "' or '" + pluralControllerName + "'.");
}
}
return controllerClass.asSubclass(ERXRouteController.class);
}
/**
* Calls the static method 'addRoutes(entityName, routeRequetHandler)' on the route controller for the given entity
* name, giving it the opportunity to add routes for this entity. Additionally, this method looks for all methods
* annotated with {@literal @}Path or {@literal @}Paths annotations and adds the corresponding routes. If no addRoutes method is found and
* no
*
* {@literal @}Path annotated methods exist, it will log a warning and add default routes instead.
*
* @param entityName
* the name of the entity
*/
public void addRoutes(String entityName) {
addRoutes(entityName, routeControllerClassForEntityNamed(entityName));
}
/**
* Calls the static method 'addRoutes(entityName, routeRequetHandler)' on the given route controller class, giving
* it the opportunity to add routes for the given entity. Additionally, this method looks for all methods annotated
* with {@literal @}Path or {@literal @}Paths annotations and adds the corresponding routes. If no addRoutes method is found and no
*
* {@literal @}Path annotated methods exist, it will log a warning and add default routes instead.
*
* @param entityName
* the name of the entity
* @param routeControllerClass
* the name of the route controller
*/
public void addRoutes(String entityName, Class<? extends ERXRouteController> routeControllerClass) {
addDeclaredRoutes(entityName, routeControllerClass, true);
}
/**
* This method looks for all methods annotated with {@literal @}Path or {@literal @}Paths annotations and adds the corresponding routes.
* If no addRoutes method is found and no {@literal @}Path annotated methods exist, it will log a warning and add default routes instead.
* This is the variant to use if you have a controller that has no logical entity associated with it.
*
* @param routeControllerClass
* the name of the route controller
*/
public void addRoutes(Class<? extends ERXRouteController> routeControllerClass) {
addDeclaredRoutes(null, routeControllerClass, true);
}
protected void addDeclaredRoutes(String entityName, Class<? extends ERXRouteController> routeControllerClass, boolean addDefaultRoutesIfNoDeclaredRoutesFound) {
boolean declaredRoutesFound = false;
try {
Method addRoutesMethod = routeControllerClass.getMethod("addRoutes", String.class, ERXRouteRequestHandler.class);
addRoutesMethod.invoke(null, entityName, this);
declaredRoutesFound = true;
}
catch (NoSuchMethodException e) {
// ignore
}
catch (Throwable t) {
throw new RuntimeException("Failed to add routes for " + routeControllerClass + ".", t);
}
for (Method routeMethod : routeControllerClass.getDeclaredMethods()) {
String routeMethodName = routeMethod.getName();
Path pathAnnotation = routeMethod.getAnnotation(Path.class);
Paths pathsAnnotation = routeMethod.getAnnotation(Paths.class);
if (pathAnnotation != null || pathsAnnotation != null) {
String actionName;
if (routeMethodName.endsWith("Action")) {
actionName = routeMethodName.substring(0, routeMethodName.length() - "Action".length());
}
else {
actionName = routeMethodName;
}
ERXRoute.Method method = null;
// Search annotations for @PUT, @GET, etc.
for (Annotation annotation : routeMethod.getAnnotations()) {
HttpMethod httpMethod = annotation.annotationType().getAnnotation(HttpMethod.class);
if (httpMethod != null) {
if (method == null) {
method = httpMethod.value();
}
else {
throw new IllegalArgumentException(routeControllerClass.getSimpleName() + "." + routeMethod.getName() + " is annotated as more than one http method.");
}
}
}
// Finally default declared REST actions to @GET
if (method == null) {
method = ERXRoute.Method.Get;
}
if (pathAnnotation != null) {
addRoute(new ERXRoute(entityName, pathAnnotation.value(), method, routeControllerClass, actionName));
declaredRoutesFound = true;
}
if (pathsAnnotation != null) {
for (Path path : pathsAnnotation.value()) {
addRoute(new ERXRoute(entityName, path.value(), method, routeControllerClass, actionName));
}
declaredRoutesFound = true;
}
}
}
if (addDefaultRoutesIfNoDeclaredRoutesFound && !declaredRoutesFound) {
log.warn("No 'addRoutes(entityName, routeRequetHandler)' method and no Path designations found on '{}'. Registering default routes instead.", routeControllerClass.getSimpleName());
addDefaultRoutes(entityName, routeControllerClass);
}
}
/**
* Adds default routes and maps them to a controller named "[plural entity name]Controller". For instance, if the
* entity name is "Person" it would make a controller named "PeopleController".
*
* @param entityName
* the name of the entity to create routes for
*/
public void addDefaultRoutes(String entityName) {
addDefaultRoutes(entityName, routeControllerClassForEntityNamed(entityName));
}
/**
* Adds list and view routes for the given entity. For instance, if you provide the entity name "Reminder" you will
* get the routes:
*
* <pre>
* /reminders
* /reminders/{action}
* /reminder/{reminder:Reminder}
* /reminder/{reminder:Reminder}/{action}
* </pre>
*
* @param entityName
* the entity name to route with
* @param controllerClass
* the controller class
*/
public void addDefaultRoutes(String entityName, Class<? extends ERXRouteController> controllerClass) {
// MS: We should probably remove all of the "boolean numericPKs" attributes for registration, but I don't
// want to do that so soon after the initial screencast of using the APIs.
EOClassDescription classDescription = ERXRestClassDescriptionFactory.classDescriptionForEntityName(entityName);
boolean numericPKs = IERXRestDelegate.Factory.delegateForClassDescription(classDescription).__hasNumericPrimaryKeys(classDescription);
addDefaultRoutes(entityName, numericPKs, controllerClass);
}
/**
* Return the controller path name for an entity name based on the entity name format.
*
* @param entityName
* the entity name
* @return the controller identifier part of the path (the "companies" part in "/companies/1000");
*/
public String controllerPathForEntityNamed(String entityName) {
return _entityNameFormat.formatEntityNamed(ERXRestNameRegistry.registry().externalNameForInternalName(entityName), true);
}
/**
* Adds list and view routes for the given entity. For instance, if you provide the entity name "Reminder" you will
* get the routes:
*
* <pre>
* /reminders
* /reminders/{action}
* /reminder/{reminder:Reminder}
* /reminder/{reminder:Reminder}/{action}
* </pre>
*
* @param entityName
* the entity name to route with
* @param numericPKs
* if true, routes can assume numeric PK's and add some extra convenience routes
* @param controllerClass
* the controller class
*/
public void addDefaultRoutes(String entityName, boolean numericPKs, Class<? extends ERXRouteController> controllerClass) {
addDefaultRoutes(entityName, entityName, numericPKs, controllerClass);
}
/**
* Adds list and view routes for the given entity. For instance, if you provide the entity name "Reminder" you will
* get the routes:
*
* <pre>
* /reminders
* /reminders/{action}
* /reminder/{reminder:Reminder}
* /reminder/{reminder:Reminder}/{action}
* </pre>
*
* @param entityName
* the entity name to route with
* @param entityType
* the type of the enity
* @param numericPKs
* if true, routes can assume numeric PK's and add some extra convenience routes
* @param controllerClass
* the controller class
*/
public void addDefaultRoutes(String entityName, String entityType, boolean numericPKs, Class<? extends ERXRouteController> controllerClass) {
NSArray<ERXRoute> existingRoutes = routesForControllerClass(controllerClass);
if (existingRoutes.isEmpty()) {
addDeclaredRoutes(entityName, controllerClass, false);
}
String variableName = ERXStringUtilities.uncapitalize(entityName); // MS: We want this to always be Java variable-style "lowerFirstLetter"
String externalName = ERXRestNameRegistry.registry().externalNameForInternalName(entityName);
String singularExternalName = _entityNameFormat.formatEntityNamed(externalName, false);
String pluralExternalName = _entityNameFormat.formatEntityNamed(externalName, true);
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName, ERXRoute.Method.Options, controllerClass, "options"));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName, ERXRoute.Method.Options, controllerClass, "options"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName, ERXRoute.Method.Head, controllerClass, "head"));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName, ERXRoute.Method.Head, controllerClass, "head"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName, ERXRoute.Method.Post, controllerClass, "create"));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName, ERXRoute.Method.Post, controllerClass, "create"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName, ERXRoute.Method.All, controllerClass, "index"));
}
else {
addRoute(new ERXRoute(entityName, "/" + singularExternalName, ERXRoute.Method.All, controllerClass, "index"));
}
if (numericPKs) {
// MS: this only works with numeric ids
addRoute(new ERXRoute(entityName, "/" + singularExternalName + "/{action:identifier}", ERXRoute.Method.Get, controllerClass));
if (_entityNameFormat.pluralRouteName()) {
// MS: this only works with numeric ids
addRoute(new ERXRoute(entityName, "/" + pluralExternalName + "/{action:identifier}", ERXRoute.Method.Get, controllerClass));
}
}
else {
addRoute(new ERXRoute(entityName, "/" + singularExternalName + "/new", ERXRoute.Method.All, controllerClass, "new"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName + "/new", ERXRoute.Method.All, controllerClass, "new"));
}
}
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName + "/{" + variableName + ":" + entityType + "}", ERXRoute.Method.Get, controllerClass, "show"));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName + "/{" + variableName + ":" + entityType + "}", ERXRoute.Method.Get, controllerClass, "show"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName + "/{" + variableName + ":" + entityType + "}", ERXRoute.Method.Put, controllerClass, "update"));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName + "/{" + variableName + ":" + entityType + "}", ERXRoute.Method.Put, controllerClass, "update"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName + "/{" + variableName + ":" + entityType + "}", ERXRoute.Method.Delete, controllerClass, "destroy"));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName + "/{" + variableName + ":" + entityType + "}", ERXRoute.Method.Delete, controllerClass, "destroy"));
if (_entityNameFormat.pluralRouteName()) {
addRoute(new ERXRoute(entityName, "/" + pluralExternalName + "/{" + variableName + ":" + entityType + "}/{action:identifier}", ERXRoute.Method.All, controllerClass));
}
addRoute(new ERXRoute(entityName, "/" + singularExternalName + "/{" + variableName + ":" + entityType + "}/{action:identifier}", ERXRoute.Method.All, controllerClass));
}
/**
* Returns the route that matches the request method and path, storing metadata about the route in the given
* userInfo dictionary.
*
* @param method
* the request method
* @param path
* the request path
* @param userInfo
* a mutable userInfo
* @return the matching route (or null if one is not found)
*/
public ERXRoute routeForMethodAndPath(String method, String path, NSMutableDictionary<String, Object> userInfo) {
if (!path.startsWith("/")) {
path = "/" + path;
}
int dotIndex = path.lastIndexOf('.');
String requestedType = null;
if (dotIndex >= 0 && path.indexOf('/', dotIndex + 1) == -1) {
String type = path.substring(dotIndex + 1);
if (_parseUnknownExtensions || ERXRestFormat.hasFormatNamed(type)) {
if (type.length() > 0) {
requestedType = type;
userInfo.setObjectForKey(type, ERXRouteRequestHandler.ExtensionKey);
}
path = path.substring(0, dotIndex);
}
}
ERXRoute.Method routeMethod = ERXRoute.Method.valueOf(ERXStringUtilities.capitalize(method.toLowerCase()));
ERXRoute matchingRoute = null;
NSDictionary<ERXRoute.Key, String> keys = null;
for (ERXRoute route : _routes) {
keys = route.keys(path, routeMethod);
if (keys != null) {
matchingRoute = route;
break;
}
}
if (matchingRoute != null) {
if (requestedType != null) {
userInfo.setObjectForKey(requestedType, ERXRouteRequestHandler.TypeKey);
}
userInfo.setObjectForKey(path, ERXRouteRequestHandler.PathKey);
userInfo.setObjectForKey(matchingRoute, ERXRouteRequestHandler.RouteKey);
userInfo.setObjectForKey(keys, ERXRouteRequestHandler.KeysKey);
}
return matchingRoute;
}
/**
* @param method
* the request method
* @param urlPattern
* @return the first route matching <code>method</code> and <code>pattern</code>.
*/
protected ERXRoute routeForMethodAndPattern(ERXRoute.Method method, String urlPattern) {
for (ERXRoute route : _routes) {
if (route.method().equals(method) && route.routePattern().pattern().equals(urlPattern)) {
return route;
}
}
return null;
}
/**
* Checks for an existing route that matches the {@link er.rest.routes.ERXRoute.Method} and {@link er.rest.routes.ERXRoute#routePattern()} of <code>route</code> and
* yet has a different controller or action mapping.
* @param route
*/
protected void verifyRoute(ERXRoute route) {
ERXRoute duplicateRoute = routeForMethodAndPattern(route.method(), route.routePattern().pattern());
if (duplicateRoute != null) {
boolean isDifferentController = ObjectUtils.notEqual(duplicateRoute.controller(), route.controller());
boolean isDifferentAction = ObjectUtils.notEqual(duplicateRoute.action(), route.action());
if (isDifferentController || isDifferentAction) {
// We have a problem whereby two routes with same url pattern and http method map to different direct actions
StringBuilder message = new StringBuilder();
message.append("The route <");
message.append(route);
message.append("> conflicts with existing route <");
message.append(duplicateRoute);
message.append(">.");
if (isDifferentController) {
message.append(" The controller class <");
message.append(route.controller());
message.append("> is different to <");
message.append(duplicateRoute.controller());
message.append(">.");
}
if (isDifferentAction) {
message.append(" The action <");
message.append(route.action());
message.append("> is different to <");
message.append(duplicateRoute.action());
message.append(">.");
}
throw new IllegalStateException(message.toString());
}
}
}
/**
* Sets up the request userInfo for the given request for a request of the given method and path.
*
* @param request
* the request to configure the userInfo on
* @param method
* the request method
* @param path
* the request path
* @return the matching route for this method and path
*/
public ERXRoute setupRequestWithRouteForMethodAndPath(WORequest request, String method, String path) {
NSDictionary<String, Object> userInfo = request.userInfo();
NSMutableDictionary<String, Object> mutableUserInfo;
if (userInfo instanceof NSMutableDictionary) {
mutableUserInfo = (NSMutableDictionary<String, Object>) userInfo;
}
else if (userInfo != null) {
mutableUserInfo = userInfo.mutableClone();
}
else {
mutableUserInfo = new NSMutableDictionary<>();
}
ERXRoute matchingRoute = routeForMethodAndPath(method, path, mutableUserInfo);
if (/*matchingRoute != null && */mutableUserInfo != userInfo) {
request.setUserInfo(mutableUserInfo);
}
return matchingRoute;
}
/**
* Sets up a route controller based on a request userInfo that came from routeForMethodAndPath.
*
* @param controller
* the controller to setup
* @param userInfo
* the request userInfo
*/
public void setupRouteControllerFromUserInfo(ERXRouteController controller, NSDictionary<String, Object> userInfo) {
controller._setRequestHandler(this);
if (userInfo != null) {
ERXRoute route = (ERXRoute) userInfo.objectForKey(ERXRouteRequestHandler.RouteKey);
controller._setRoute(route);
@SuppressWarnings("unchecked")
NSDictionary<ERXRoute.Key, String> keys = (NSDictionary<ERXRoute.Key, String>) userInfo.objectForKey(ERXRouteRequestHandler.KeysKey);
controller._setRouteKeys(keys);
}
}
@Override
public NSArray<String> getRequestHandlerPathForRequest(WORequest request) {
NSMutableArray<String> requestHandlerPath = new NSMutableArray<>();
try {
String path = request._uriDecomposed().requestHandlerPath();
String method = request.headerForKey(X_HTTP_METHOD_OVERRIDE_HEADER_KEY, request.method());
ERXRoute matchingRoute = setupRequestWithRouteForMethodAndPath(request, method, path);
if (matchingRoute != null) {
@SuppressWarnings("unchecked")
NSDictionary<ERXRoute.Key, String> keys = (NSDictionary<ERXRoute.Key, String>) request.userInfo().objectForKey(ERXRouteRequestHandler.KeysKey);
String controller = keys.objectForKey(ERXRoute.ControllerKey);
String actionName = keys.objectForKey(ERXRoute.ActionKey);
requestHandlerPath.addObject(controller);
requestHandlerPath.addObject(actionName);
}
else {
requestHandlerPath.addObject(ERXProperties.stringForKeyWithDefault("ERXRest.missingControllerName", "ERXMissingRouteController"));
requestHandlerPath.addObject("missing");
// throw new FileNotFoundException("There is no controller for the route '" + path + "'.");
}
}
catch (Throwable t) {
throw new RuntimeException("Failed to process the requested route.", t);
}
return requestHandlerPath;
}
@Override
public Object[] getRequestActionClassAndNameForPath(NSArray requestHandlerPath) {
String requestActionClassName = (String) requestHandlerPath.objectAtIndex(0);
String requestActionName = (String) requestHandlerPath.objectAtIndex(1);
return new Object[] { requestActionClassName, requestActionName, _NSUtilities.classWithName(requestActionClassName) };
}
@Override
public WOAction getActionInstance(Class class1, Class[] aclass, Object[] aobj) {
ERXRouteController controller = (ERXRouteController) super.getActionInstance(class1, aclass, aobj);
WORequest request = (WORequest) aobj[0];
setupRouteControllerFromUserInfo(controller, request.userInfo());
return controller;
}
/**
* Returns the corresponding controller instance.
*
* @param <T>
* the type of controller to return
* @param entityName
* the entity name of the controller to lookup
* @param request the current request
* @param context the current context
* @return the created controller
*/
@SuppressWarnings("unchecked")
public <T extends ERXRouteController> T controller(String entityName, WORequest request, WOContext context) {
return controller((Class<T>) routeControllerClassForEntityNamed(entityName), request, context);
}
/**
* Returns the corresponding controller instance (with no request specified).
*
* @param <T>
* the type of controller to return
* @param controllerClass
* the controller class to lookup
* @param context the current context
* @return the created controller
*/
public <T extends ERXRouteController> T controller(Class<T> controllerClass, WOContext context) {
return controller(controllerClass, null, context);
}
/**
* Returns the corresponding controller instance.
*
* @param <T>
* the type of controller to return
* @param controllerClass
* the controller class to lookup
* @param request the current request
* @param context the current context
* @return the created controller
*/
public <T extends ERXRouteController> T controller(Class<T> controllerClass, WORequest request, WOContext context) {
try {
T controller = controllerClass.getConstructor(WORequest.class).newInstance(request);
controller._setRequestHandler(this);
controller._setContext(context);
return controller;
}
catch (Exception e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
}
}
@Override
public void _putComponentsToSleepInContext(WOContext wocontext) {
super._putComponentsToSleepInContext(wocontext);
ERXRouteController._disposeControllersForRequest(wocontext.request());
}
/**
* Registers an ERXRestRequestHandler with the WOApplication for the handler key "rest".
*
* @param requestHandler
* the rest request handler to register
*/
public static void register(ERXRouteRequestHandler requestHandler) {
WOApplication.application().registerRequestHandler(requestHandler, ERXRouteRequestHandler.Key);
}
}