package er.rest.routes;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation._NSUtilities;
import er.rest.ERXRestContext;
import er.rest.ERXRestUtils;
/**
* ERXRoute encapsulates a URL path with matching values inside of it. For instance, the route
* "/company/{company:Company}/employees/{Person}/name/{name:String}" would yield an objects(..) dictionary with a
* Company EO mapped to the key "company," a Person EO mapped to the key "Person" and a String mapped to the key "name".
* ERXRoutes do not enforce any security -- they simply represent a way to map URL patterns onto objects.
*
* @author mschrag
*/
public class ERXRoute {
public static enum Method {
All, Get, Put, Post, Delete, Head, Options, Trace, Connect
}
public static final ERXRoute.Key ControllerKey = new ERXRoute.Key("controller");
public static final ERXRoute.Key ActionKey = new ERXRoute.Key("action");
private final String _entityName;
private final Pattern _routePattern;
private ERXRoute.Method _method;
private final NSMutableArray<ERXRoute.Key> _keys;
private final Class<? extends ERXRouteController> _controller;
private final String _action;
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param method
* the request method
*/
public ERXRoute(String entityName, String urlPattern, ERXRoute.Method method) {
this(entityName, urlPattern, method, (Class<? extends ERXRouteController>) null, null);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
*/
public ERXRoute(String entityName, String urlPattern) {
this(entityName, urlPattern, ERXRoute.Method.All, (Class<? extends ERXRouteController>) null, null);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param controller
* the default controller class name
*/
@SuppressWarnings("unchecked")
public ERXRoute(String entityName, String urlPattern, String controller) {
this(entityName, urlPattern, ERXRoute.Method.All, _NSUtilities.classWithName(controller).asSubclass(ERXRouteController.class), null);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param method
* the request method
* @param controller
* the default controller class name
*/
@SuppressWarnings("unchecked")
public ERXRoute(String entityName, String urlPattern, ERXRoute.Method method, String controller) {
this(entityName, urlPattern, method, _NSUtilities.classWithName(controller).asSubclass(ERXRouteController.class), null);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param controller
* the default controller class
*/
public ERXRoute(String entityName, String urlPattern, Class<? extends ERXRouteController> controller) {
this(entityName, urlPattern, ERXRoute.Method.All, controller, null);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param method
* the request method
* @param controller
* the default controller class
*/
public ERXRoute(String entityName, String urlPattern, ERXRoute.Method method, Class<? extends ERXRouteController> controller) {
this(entityName, urlPattern, method, controller, null);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param controller
* the default controller class name
* @param action
* the action name
*/
@SuppressWarnings("unchecked")
public ERXRoute(String entityName, String urlPattern, String controller, String action) {
this(entityName, urlPattern, ERXRoute.Method.All, _NSUtilities.classWithName(controller).asSubclass(ERXRouteController.class), action);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param method
* the request method
* @param controller
* the default controller class name
* @param action
* the action name
*/
@SuppressWarnings("unchecked")
public ERXRoute(String entityName, String urlPattern, ERXRoute.Method method, String controller, String action) {
this(entityName, urlPattern, method, _NSUtilities.classWithName(controller).asSubclass(ERXRouteController.class), action);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param controller
* the default controller class
* @param action
* the action name
*/
public ERXRoute(String entityName, String urlPattern, Class<? extends ERXRouteController> controller, String action) {
this(entityName, urlPattern, ERXRoute.Method.All, controller, action);
}
/**
* Constructs a new route with the given URL pattern.
*
* @param entityName
* the name of the entity this route points to
* @param urlPattern
* the URL pattern to use
* @param method
* the request method
* @param controller
* the default controller class
* @param action
* the action name
*/
public ERXRoute(String entityName, String urlPattern, ERXRoute.Method method, Class<? extends ERXRouteController> controller, String action) {
_entityName = entityName;
_method = method;
_controller = controller;
_action = action;
Matcher keyMatcher = Pattern.compile("\\{([^}]+)\\}").matcher(urlPattern);
_keys = new NSMutableArray<>();
StringBuffer routeRegex = new StringBuffer();
if (!urlPattern.startsWith("^")) {
routeRegex.append('^');
}
while (keyMatcher.find()) {
String keyStr = keyMatcher.group(1);
String replacement = "([^/]+)";
ERXRoute.Key key = new ERXRoute.Key();
int colonIndex = keyStr.indexOf(':');
if (colonIndex == -1) {
key._key = keyStr;
if (Character.isUpperCase(keyStr.charAt(0))) {
key._valueType = keyStr;
}
else {
key._valueType = String.class.getName();
}
}
else {
String[] segments = keyStr.split(":");
key._key = segments[0];
key._valueType = segments[1];
if (segments.length == 3) {
replacement = "(" + segments[2].replaceAll("[\\\\$]", "\\\\$0") + ")";
}
}
if ("identifier".equals(key._valueType)) {
key._valueType = String.class.getName();
replacement = "(\\\\D[^/-]*)";
}
_keys.addObject(key);
keyMatcher.appendReplacement(routeRegex, replacement);
}
keyMatcher.appendTail(routeRegex);
if (!urlPattern.endsWith("$")) {
if (routeRegex.lastIndexOf(".") == -1) {
routeRegex.append("/?(\\..*)?");
}
routeRegex.append('$');
}
_routePattern = Pattern.compile(routeRegex.toString());
}
/**
* Returns the entity name of the target of this route (can be null).
*
* @return the entity name of the target of this route
*/
public String entityName() {
return _entityName;
}
/**
* Returns the controller class for this route.
*
* @return the controller class for this route
*/
public Class<? extends ERXRouteController> controller() {
return _controller;
}
/**
* @return the controller action name for this route.
*/
public String action() {
return _action;
}
/**
* Returns the Pattern used to match this route.
*
* @return the Pattern used to match this route
*/
public Pattern routePattern() {
return _routePattern;
}
/**
* Returns the method of this request.
*
* @return the method of this request
*/
public ERXRoute.Method method() {
return _method;
}
/**
* Sets the method of this request.
*
* @param method
* the method of this request
*/
public void setMethod(ERXRoute.Method method) {
_method = method;
}
/**
* 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.Key key : _keys) {
key._clearRouteParameterMethodCache();
}
ERXRoute.ControllerKey._clearRouteParameterMethodCache();
ERXRoute.ActionKey._clearRouteParameterMethodCache();
}
/**
* Returns the route keys for the given URL.
*
* @param url
* the URL to parse
* @param method
* the request method
*/
public NSDictionary<ERXRoute.Key, String> keys(String url, ERXRoute.Method method) {
NSMutableDictionary<ERXRoute.Key, String> keys = null;
if (_method == ERXRoute.Method.All || _method == null || method == null || method.equals(_method)) {
Matcher routeMatcher = _routePattern.matcher(url);
if (routeMatcher.matches()) {
keys = new NSMutableDictionary<>();
int keyCount = _keys.count();
for (int keyNum = 0; keyNum < keyCount; keyNum++) {
ERXRoute.Key key = _keys.objectAtIndex(keyNum);
String value = routeMatcher.group(keyNum + 1);
keys.setObjectForKey(value, key);
}
if (!keys.containsKey(ERXRoute.ControllerKey) && _controller != null) {
keys.setObjectForKey(_controller.getName(), ERXRoute.ControllerKey);
}
if (!keys.containsKey(ERXRoute.ActionKey) && _action != null) {
keys.setObjectForKey(_action, ERXRoute.ActionKey);
}
}
}
return keys;
}
/**
* Returns a dictionary mapping the route's keys to their resolved objects.
*
* @param url
* the URL to process
* @param method
* the request method
* @param context
* the delegate to use to, for instance, fault EO's with (or null to not fault EO's)
* @return a dictionary mapping the route's keys to their resolved objects
*/
public NSDictionary<ERXRoute.Key, Object> keysWithObjects(String url, ERXRoute.Method method, ERXRestContext context) {
return keysWithObjects(keys(url, method), context);
}
/**
* Returns a dictionary mapping the route's key names to their resolved objects.
*
* @param url
* the URL to process
* @param method
* the request method
* @param context
* the delegate to use to, for instance, fault EO's with (or null to not fault EO's)
* @return a dictionary mapping the route's key names to their resolved objects
*/
public NSDictionary<String, Object> objects(String url, ERXRoute.Method method, ERXRestContext context) {
return objects(keys(url, method), context);
}
/**
* Returns a dictionary mapping the route's keys to their resolved objects.
*
* @param keys
* the parsed keys to process
* @param context
* the delegate to use to, for instance, fault EO's with (or null to not fault EO's)
* @return a dictionary mapping the route's keys to their resolved objects
*/
public static NSDictionary<ERXRoute.Key, Object> keysWithObjects(NSDictionary<ERXRoute.Key, String> keys, ERXRestContext context) {
NSMutableDictionary<ERXRoute.Key, Object> objects = null;
if (keys != null) {
objects = new NSMutableDictionary<>();
for (Map.Entry<ERXRoute.Key, String> entry : keys.entrySet()) {
ERXRoute.Key key = entry.getKey();
String valueStr = entry.getValue();
Object value = ERXRestUtils.coerceValueToTypeNamed(valueStr, key.valueType(), context, true);
if (value != null) {
objects.setObjectForKey(value, key);
} else {
objects = new NSMutableDictionary<>();
}
}
}
else {
objects = new NSMutableDictionary<>();
}
return objects;
}
/**
* Returns a dictionary mapping the route's key names to their resolved objects.
*
* @param keys
* the parsed keys to process
* @param context
* the delegate to use to, for instance, fault EO's with (or null to not fault EO's)
* @return a dictionary mapping the route's key names to their resolved objects
*/
public NSDictionary<String, Object> objects(NSDictionary<ERXRoute.Key, String> keys, ERXRestContext context) {
NSMutableDictionary<String, Object> objects = null;
if (keys != null) {
objects = new NSMutableDictionary<>();
for (Map.Entry<ERXRoute.Key, String> entry : keys.entrySet()) {
ERXRoute.Key key = entry.getKey();
String valueStr = entry.getValue();
Object value = ERXRestUtils.coerceValueToTypeNamed(valueStr, key.valueType(), context, true);
objects.setObjectForKey(value, key._key);
}
}
return objects;
}
@Override
public String toString() {
return "[ERXRoute: pattern=" + _routePattern + "; method=" + _method + "; controller=" + _controller + "; action=" + _action + "; keys=" + _keys.valueForKey("key") + "]";
}
/**
* ERXRoute.Key encapsulates a key name and an expected value type.
*
* @author mschrag
*/
public static class Key {
protected String _valueType;
protected String _key;
private final Map<Class<?>, RouteParameterMethod> _routeParameterMethodCache;
public Key(String key) {
this(key, String.class.getName());
}
public Key(String key, String valueType) {
this();
_key = key;
_valueType = valueType;
}
protected Key() {
_routeParameterMethodCache = new ConcurrentHashMap<>();
}
public String key() {
return _key;
}
public String valueType() {
return _valueType;
}
public void _clearRouteParameterMethodCache() {
_routeParameterMethodCache.clear();
}
public RouteParameterMethod _routeParameterMethodForClass(Class<?> resultsClass) {
return _routeParameterMethodCache.get(resultsClass);
}
public void _setRouteParameterMethodForClass(RouteParameterMethod routeParameter, Class<?> resultsClass) {
_routeParameterMethodCache.put(resultsClass, routeParameter);
}
@Override
public int hashCode() {
return _key.hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof ERXRoute.Key && ((ERXRoute.Key) obj)._key.equals(_key);
}
@Override
public String toString() {
return "[ERXRoute.Key: " + _key + "]";
}
}
public static class RouteParameterMethod {
private final java.lang.reflect.Method _method;
private boolean _stringParameter;
public RouteParameterMethod(java.lang.reflect.Method method) {
_method = method;
if (_method != null) {
Class<?>[] parameterTypes = _method.getParameterTypes();
if (parameterTypes.length != 1) {
throw new IllegalArgumentException("The route parameter method '" + method + "' must take a single parameter.");
}
_stringParameter = String.class.isAssignableFrom(parameterTypes[0]);
}
}
public boolean isStringParameter() {
return _stringParameter;
}
public boolean hasMethod() {
return _method != null;
}
public java.lang.reflect.Method method() {
return _method;
}
@Override
public String toString() {
return "[ERXRoute.RouteParameterMethod: method=" + (_method == null ? "(none)" : _method.toString()) + "]";
}
}
}