/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.web.route;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import com.scooterframework.admin.EnvConfig;
import com.scooterframework.common.logging.LogUtil;
import com.scooterframework.common.util.Converters;
import com.scooterframework.common.util.PropertyFileUtil;
import com.scooterframework.common.util.StringUtil;
import com.scooterframework.common.util.Util;
import com.scooterframework.common.util.WordUtil;
import com.scooterframework.orm.sqldataexpress.config.DatabaseConfig;
/**
* Route class
*
* @author (Fei) John Chen
*/
abstract public class Route {
private LogUtil log = LogUtil.getLogger(this.getClass().getName());
protected String name;
protected String urlPattern;
protected String controller;
protected String controllerClass;
protected String action;
protected String id;
protected String format;
protected String allowed_formats = RouteConstants.ROUTE_DEFAULT_ALLOWED_FORMATS;
protected String allowed_methods = RouteConstants.ROUTE_DEFAULT_ALLOWED_METHODS;
protected String singular;
protected String namespace;
protected String pathPrefix;
protected String requirements;
protected String cacheable;
protected boolean dynamicController;
protected boolean dynamicAction;
protected boolean dynamicFormat;
private Map<String, Integer> requiredFieldPositions = new HashMap<String, Integer>();
private String[] pathSegments;
private int segmentCount;
private String screenURLPattern;
private Properties requirementsProperties;
protected Route() {
;
}
public Route(String name, Properties p) {
this.name = name;
if (name == null)
throw new IllegalArgumentException("Name cannot be empty in route constructor(String, Properties).");
populateProperties(p);
}
abstract public String getRouteType();
abstract protected boolean isRouteFor(RequestInfo requestInfo);
public RouteInfo getRouteInfo(RequestInfo requestInfo) {
RouteInfo ri = new RouteInfo(requestInfo);
Map<String, String> fieldValues = new HashMap<String, String>();
if (requiredFieldPositions.size() > 0) {
for (Map.Entry<String, Integer> entry : requiredFieldPositions.entrySet()) {
String field = entry.getKey();
Integer iPos = entry.getValue();
if (iPos == null) {
log.error("There is position value for field '" + field +
"' in RequestInfo: " + requestInfo);
continue;
}
int position = iPos.intValue();
String value = requestInfo.getPathSegments()[position];
if (field.indexOf(RouteConstants.PRIMARY_KEY_SEPARATOR) != -1) {
fieldValues.putAll(getFieldValueMapForCompositeKey(field, value));
}
else {
fieldValues.put(field, value);
}
}
}
String tmp = "";
tmp = fieldValues.get(RouteConstants.ROUTE_KEY_CONTROLLER);
tmp = (tmp != null)?tmp:this.controller;
tmp = (namespace != null)?(namespace + "/" + tmp):tmp;
ri.controller = tmp;
tmp = fieldValues.get(RouteConstants.ROUTE_KEY_ACTION);
ri.action = (tmp != null)?tmp:this.action;
tmp = fieldValues.get(RouteConstants.ROUTE_KEY_ID);
ri.id = (tmp != null)?tmp:this.id;
ri.requiredFieldValues = fieldValues;
ri.controllerClassName = getControllerClassName(ri.controller);
ri.model = getModel(ri.controller);
ri.modelClassName = getModelClassName(ri.controller);
ri.format = requestInfo.getFormat();
ri.routeType = getRouteType();
ri.routeName = getName();
ri.viewPath = getViewPath(ri.controller);
ri.cacheable = getCacheable();
return ri;
}
private static Map<String, String> getFieldValueMapForCompositeKey(String compositeFields, String restfulId) {
if (restfulId == null)
throw new IllegalArgumentException("restfulId cannot be null in getFieldValueMapForCompositeKey().");
String[] fields = Converters.convertStringToStringArray(compositeFields, DatabaseConfig.PRIMARY_KEY_SEPARATOR);
String[] values = Converters.convertStringToStringArray(restfulId, DatabaseConfig.PRIMARY_KEY_SEPARATOR, false);
if (fields.length != values.length) {
if (fields.length == 1) {
values[0] = restfulId;
}
else {
throw new IllegalArgumentException("Input restfulId value \"" +
restfulId + "\" with length " + values.length + " does not " +
"match key fields of its related table with length " +
fields.length + ".");
}
}
int total = fields.length;
Map<String, String> map = new HashMap<String, String>(total);
for (int i = 0; i < total; i++) {
String field = fields[i];
String value = values[i];
if (field.startsWith("$")) field = field.substring(1);
map.put(field.toUpperCase(), value);
}
return map;
}
public String getName() {
return name;
}
public String getURLPattern() {
return urlPattern;
}
public String getController() {
return controller;
}
public String getControllerClass() {
return controllerClass;
}
protected String getControllerClassName(String controller) {
String ccn = "";
if (controllerClass != null) {
ccn = controllerClass;
}
else {
ccn = controller.replace('/', '.');
ccn = (namespace != null)?(namespace + "." + ccn):ccn;
ccn = EnvConfig.getInstance().getControllerClassName(ccn);
}
return ccn;
}
public String getAction() {
return action;
}
public String getId() {
return id;
}
public String getFormat() {
return format;
}
public boolean hasFormat() {
return (format != null)?true:false;
}
public String getAllowedFormats() {
return allowed_formats;
}
public String[] allowedFormats() {
return (allowed_formats != null)?Converters.convertStringToStringArray(allowed_formats, RouteConstants.PROPERTY_SYMBOL_ARRAY_ITEMS_DELIMITER):null;
}
protected boolean isAllowedFormat(String fmat) {
boolean allowed = false;
if (allowed_formats != null) {
if (fmat != null && allowed_formats.toLowerCase().indexOf(fmat.toLowerCase()) != -1) {
allowed = true;
}
}
else {
if (format == null) {
if (fmat == null) {
allowed = true;
}
}
else {
if (fmat != null) {
if (fmat.equalsIgnoreCase(format) || dynamicFormat) allowed = true;
}
}
}
return allowed;
}
public String getAllowedMethods() {
return allowed_methods;
}
public String[] allowedMethods() {
String[] sa = Converters.convertStringToStringArray(allowed_methods, RouteConstants.PROPERTY_SYMBOL_ARRAY_ITEMS_DELIMITER);
return sa;
}
public static void validateMethods(String httpMethods) {
String[] sa = Converters.convertStringToStringArray(httpMethods, RouteConstants.PROPERTY_SYMBOL_ARRAY_ITEMS_DELIMITER);
if (sa == null) return;
int length = sa.length;
for (int i = 0; i < length; i++) {
String m = sa[i];
if (m != null && RouteConstants.ROUTE_HTTP_ALL_METHODS.indexOf(m.toUpperCase()) == -1) {
throw new IllegalArgumentException("Method \"" + m + "\" in string \"" + httpMethods + "\" is not a supported HTTP methods.");
}
}
}
protected boolean isAllowedMethod(String method) {
if (method == null) return false;
boolean allowed = false;
if (allowed_methods != null) {
if (allowed_methods.toUpperCase().indexOf(RouteConstants.ROUTE_HTTP_METHOD_ANY) != -1) {
allowed = true;
}
else
if (allowed_methods.toUpperCase().indexOf(method.toUpperCase()) != -1) {
allowed = true;
}
}
else {
allowed = true;
}
return allowed;
}
public String getSingular() {
return singular;
}
protected String getModel(String controller) {
String model = null;
if (singular != null) {
model = singular;
}
else {
model = controller;
int lastSlash = controller.lastIndexOf('/');
if (lastSlash != -1) {
model = model.substring(lastSlash + 1);
}
int lastDot = controller.lastIndexOf('.');
if (lastDot != -1) {
model = model.substring(lastDot + 1);
}
model = (DatabaseConfig.getInstance().usePluralTableName())?WordUtil.singularize(model):model;
}
return model;
}
public String getModelClassName(String controller) {
return EnvConfig.getInstance().getModelClassName(getModel(controller));
}
public String getNamespace() {
return namespace;
}
public String getPathPrefix() {
return pathPrefix;
}
public String getRequirements() {
return requirements;
}
public String getCacheable() {
return cacheable;
}
/**
* Returns screen URL which is a combination of <tt>path_prefix</tt> and
* <tt>url</tt>.
*/
protected String getScreenURLPattern() {
return screenURLPattern;
}
public String getScreenURL(Map<String, String> fieldValues) {
return resolveURL(getScreenURLPattern(), fieldValues);
}
static String resolveURL(String urlPattern, Map<String, String> fieldValues) {
if (fieldValues == null || fieldValues.size() == 0 ||
urlPattern == null || urlPattern.indexOf("$") == -1) return urlPattern;
for (Map.Entry<String, String> entry : fieldValues.entrySet()) {
String field = entry.getKey();
String value = entry.getValue();
if (urlPattern.indexOf(field) == -1) continue;
value = StringUtil.replace(value, "$", "\\$");
if (value == null)
throw new IllegalArgumentException("There is no value " +
"provided for field \"" + field + "\" in url \"" +
urlPattern + "\". Provided field/value pairs are " + fieldValues + ".");
urlPattern = urlPattern.replaceAll("\\$" + field, value);
}
return urlPattern;
}
/**
* Path to the view file
*/
public String getViewPath(String controller) {
return controller;
}
public String[] getPathSegments() {
return Util.cloneArray(pathSegments);
}
public int segmentCount() {
return segmentCount;
}
public Map<String, Integer> getRequiredFieldPositions() {
return requiredFieldPositions;
}
protected boolean isAllowedFieldValue(RequestInfo requestInfo) {
//no restrictions
if (requirementsProperties == null || requirementsProperties.size() == 0) return true;
if (requiredFieldPositions.size() > 0) {
for (Map.Entry<String, Integer> entry : requiredFieldPositions.entrySet()) {
String field = entry.getKey();
String requirementStr = requirementsProperties.getProperty(field);
if (requirementStr == null) continue;
Integer iPos = entry.getValue();
if (iPos == null) {
log.error("There is position value for field '" + field +
"' in RequestInfo: " + requestInfo);
continue;
}
int position = iPos.intValue();
String value = requestInfo.getPathSegments()[position];
if (!matchRequirement(requirementStr, value)) return false;
}
}
return true;
}
private boolean matchRequirement(String requirementStr, String input) {
if (requirementStr == null) return true;
if (input == null) return false;
boolean result = false;
if (requirementStr.startsWith("/") && requirementStr.endsWith("/")) {
//apply Pattern
requirementStr = requirementStr.substring(1, requirementStr.length() -1);
result = Pattern.matches(requirementStr, input);
}
return result;
}
public String getURLSegment(String key, String path) {
int position = ((Integer)requiredFieldPositions.get(key)).intValue();
String s = path;
if (path.startsWith("/")) s = path.substring(1);
String[] segs = s.split("/");
if (segmentCount != segs.length)
throw new IllegalArgumentException("The number of segments of the input path does not match what is required by this route.");
return segs[position];
}
public void copy(Route route) {
name = route.getName();
urlPattern = route.getURLPattern();
controller = route.getController();
controllerClass = route.getControllerClass();
action = route.getAction();
id = route.getId();
format = route.getFormat();
allowed_formats = route.getAllowedFormats();
allowed_methods = route.getAllowedMethods();
singular = route.getSingular();
namespace = route.getNamespace();
pathPrefix = route.getPathPrefix();
requirements = route.getRequirements();
cacheable = route.cacheable;
dynamicController = route.dynamicController;
dynamicAction = route.dynamicAction;
dynamicFormat = route.dynamicFormat;
requiredFieldPositions = route.getRequiredFieldPositions();
pathSegments = route.getPathSegments();
segmentCount = route.segmentCount();
}
/**
* Returns a string representation of the object.
* @return String
*/
public String toString() {
StringBuilder returnString = new StringBuilder();
String SEPARATOR = ", ";
returnString.append("name = " + name).append(SEPARATOR);
returnString.append("routeType = " + getRouteType()).append(SEPARATOR);
returnString.append("url = " + urlPattern).append(SEPARATOR);
returnString.append("controller = " + controller).append(SEPARATOR);
returnString.append("controllerClass = " + controllerClass).append(SEPARATOR);
returnString.append("dynamicController = " + dynamicController).append(SEPARATOR);
returnString.append("action = " + action).append(SEPARATOR);
returnString.append("dynamicAction = " + dynamicAction).append(SEPARATOR);
returnString.append("id = " + id).append(SEPARATOR);
returnString.append("format = " + format).append(SEPARATOR);
returnString.append("dynamicFormat = " + dynamicFormat).append(SEPARATOR);
returnString.append("allowed_formats = " + allowed_formats).append(SEPARATOR);
returnString.append("allowed_methods = " + allowed_methods).append(SEPARATOR);
returnString.append("singular = " + singular).append(SEPARATOR);
returnString.append("namespace = " + namespace).append(SEPARATOR);
returnString.append("pathPrefix = " + pathPrefix).append(SEPARATOR);
returnString.append("requirements = " + requirements).append(SEPARATOR);
returnString.append("cacheable = " + cacheable).append(SEPARATOR);
returnString.append("requiredFieldPositions = " + requiredFieldPositions).append(SEPARATOR);
returnString.append("segmentCount = " + segmentCount);
return returnString.toString();
}
protected void populateProperties(Properties p) {
urlPattern = p.getProperty(RouteConstants.ROUTE_KEY_URL);
if (urlPattern == null)
throw new IllegalArgumentException("url cannot be empty in route named " + name + ".");
controller = p.getProperty(RouteConstants.ROUTE_KEY_CONTROLLER);
controllerClass = p.getProperty(RouteConstants.ROUTE_KEY_CONTROLLER_CLASS);
action = p.getProperty(RouteConstants.ROUTE_KEY_ACTION);
id = p.getProperty(RouteConstants.ROUTE_KEY_ID);
allowed_formats = p.getProperty(RouteConstants.ROUTE_KEY_ALLOWED_FORMATS, allowed_formats);
allowed_formats = StringUtil.remove(allowed_formats, RouteConstants.PROPERTY_SYMBOL_ARRAY);
allowed_methods = p.getProperty(RouteConstants.ROUTE_KEY_ALLOWED_METHODS, allowed_methods);
allowed_methods = StringUtil.remove(allowed_methods, RouteConstants.PROPERTY_SYMBOL_ARRAY);
singular = p.getProperty(RouteConstants.ROUTE_KEY_SINGULAR);
namespace = p.getProperty(RouteConstants.ROUTE_KEY_NAMESPACE);
pathPrefix = p.getProperty(RouteConstants.ROUTE_KEY_PATH_PREFIX);
cacheable = p.getProperty(RouteConstants.ROUTE_KEY_CACHEABLE);
//
//parse requirements properties
//
requirements = p.getProperty(RouteConstants.ROUTE_KEY_REQUIREMENTS);
requirements = StringUtil.remove(requirements, RouteConstants.PROPERTY_SYMBOL_GROUP);
if (requirements != null) {
requirementsProperties =
PropertyFileUtil.parseNestedPropertiesFromLine(requirements,
RouteConstants.PROPERTY_SYMBOL_GROUP_ITEM_ASSIGN,
RouteConstants.PROPERTY_SYMBOL_GROUP_ITEMS_DELIMITER);
}
screenURLPattern = urlPattern;
if (pathPrefix != null && !"".equals(pathPrefix)) {
if (!screenURLPattern.startsWith("/")) screenURLPattern = "/" + screenURLPattern;
screenURLPattern = pathPrefix + screenURLPattern;
}
parsePath(screenURLPattern);
populateRequiredFields();
validation();
}
protected void parsePath(String path) {
if ("".equals(path) || "/".equals(path)) {
segmentCount = 0;
}
else {
String s = path;
if (path.startsWith("/")) s = path.substring(1);
int lastDot = s.lastIndexOf('.');
int lastSlash = s.lastIndexOf('/');
if (lastDot > lastSlash) {
format = s.substring(lastDot + 1);
s = s.substring(0, lastDot);
}
pathSegments = s.split("/");
segmentCount = pathSegments.length;
}
if (RouteConstants.ROUTE_DEFAULT_FORMAT.equals(format)) dynamicFormat = true;
}
protected void populateRequiredFields() {
int length = segmentCount;
for (int i = 0; i < length; i++) {
String element = pathSegments[i];
if (element.startsWith("$")) {
String elementName = element.substring(1);
if (RouteConstants.ROUTE_KEY_CONTROLLER.equals(elementName)) {
if (controller != null) {
throw new IllegalArgumentException("Wrong route definition: controller is already specified.");
}
else {
dynamicController = true;
requiredFieldPositions.put(RouteConstants.ROUTE_KEY_CONTROLLER, Integer.valueOf(i));
}
}
else
if (RouteConstants.ROUTE_KEY_ACTION.equals(elementName)) {
if (action != null) {
throw new IllegalArgumentException("Wrong route definition: action is already specified.");
}
else {
dynamicAction = true;
requiredFieldPositions.put(RouteConstants.ROUTE_KEY_ACTION, Integer.valueOf(i));
}
}
else {
requiredFieldPositions.put(elementName, Integer.valueOf(i));
}
}
}
}
protected void validation() {
if (!dynamicController && (controller == null && controllerClass == null)) {
throw new IllegalArgumentException("controller cannot be empty in route named " + name + ".");
}
if (!dynamicAction && action == null) {
throw new IllegalArgumentException("action cannot be empty in route named " + name + ".");
}
validateMethods(allowed_methods);
}
}