/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.bunjlabs.fuga.router; import com.bunjlabs.fuga.foundation.http.RequestMethod; import com.bunjlabs.fuga.foundation.controllers.DefaultController; import com.bunjlabs.fuga.resources.ResourceRepresenter; import static com.bunjlabs.fuga.router.Tokenizer.*; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class RouteMapLoader { private final Logger log = LogManager.getLogger(RouteMapLoader.class); private final ResourceRepresenter resourceRepresenter; private Tokenizer t; private final List<String> uses = new ArrayList<>(); private final Class defaultController = DefaultController.class; private List<Extension> extensions; public RouteMapLoader(ResourceRepresenter resourceRepresenter) { this.resourceRepresenter = resourceRepresenter; uses.add(""); } private Class tryLoadClass(String name) { try { return Class.forName(name.startsWith(".") ? name.substring(1) : name); } catch (Exception ex) { return null; } } private Method tryGetMethod(Class c, String name, Class[] classes) { try { return c.getMethod(name, classes); } catch (NoSuchMethodException | SecurityException ex) { return null; } } private RouteParameter parameter() throws RoutesMapSyntaxException { String value; String type = "String"; if (t.ttype != TK_INTEGER && t.ttype != TK_STRCONST) { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } boolean isConst = t.ttype == TK_STRCONST; value = t.sval; t.next(); if (t.ttype == ':') { t.next(); if (t.ttype != TK_WORD) { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } type = t.sval; t.next(); } if (isConst) { return new RouteParameter(value, type); } else { return new RouteParameter(Integer.parseInt(value), type); } } private Route route() throws RoutesMapSyntaxException, RoutesMapLoadException { String classMethodFull = t.sval; t.next(); if (t.ttype != '(') { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } t.next(); List<RouteParameter> parameters = new ArrayList<>(); for (;;) { if (t.ttype == ')') { t.next(); break; } parameters.add(parameter()); if (t.ttype == ',') { t.next(); } } Class[] classes = new Class[parameters.size()]; for (int i = 0; i < classes.length; i++) { try { classes[i] = getClassTypeBySimpleName(parameters.get(i).getDataType()); } catch (ClassNotFoundException ex) { throw new RoutesMapLoadException(t, "Unable to cast parameter " + i, ex); } } if (classMethodFull.trim().isEmpty()) { throw new RoutesMapLoadException(t, "Method name cannot be empty!"); } int lio = classMethodFull.lastIndexOf('.'); String className; String classMethod; if (lio < 0) { className = ""; classMethod = classMethodFull; switch (classMethod) { case "view": return new Route(defaultController, tryGetMethod(defaultController, "generateView", classes), parameters); case "asset": return new Route(defaultController, tryGetMethod(defaultController, "generateAsset", classes), parameters); case "notFound": return new Route(defaultController, tryGetMethod(defaultController, "generateNotFound", classes), parameters); case "seeOther": return new Route(defaultController, tryGetMethod(defaultController, "generateSeeOther", classes), parameters); case "ok": return new Route(defaultController, tryGetMethod(defaultController, "generateOk", classes), parameters); } } else { className = classMethodFull.substring(0, lio); classMethod = classMethodFull.substring(lio + 1); } Class c; Method m; for (String use : uses) { if ((c = tryLoadClass(className.isEmpty() ? use : use + "." + className)) != null) { if ((m = tryGetMethod(c, classMethod, classes)) != null) { return new Route(c, m, parameters); } } } throw new RoutesMapLoadException(t, "Method not found: " + className + "." + classMethod); } private Extension extension() throws RoutesMapLoadException, RoutesMapSyntaxException { Set<RequestMethod> methods = EnumSet.noneOf(RequestMethod.class); Pattern pattern = null; Pattern host = null; boolean patternAccumulator = false; for (;;) { if (t.ttype == TK_METHOD) { methods.add(RequestMethod.valueOf(t.sval)); t.next(); } else if (t.ttype == TK_PATTERN) { if (t.sval.startsWith("!")) { patternAccumulator = true; pattern = Pattern.compile(t.sval.substring(1)); } else { pattern = Pattern.compile(t.sval); } t.next(); } else if (t.ttype == TK_HOST) { t.next(); if (t.ttype != TK_PATTERN) { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } host = Pattern.compile(t.sval); t.next(); } else if (t.ttype == '{') { t.next(); return new Extension(methods, pattern, host, patternAccumulator, extensionList()); } else if (t.ttype == TK_WORD) { return new Extension(methods, pattern, host, patternAccumulator, route()); } else { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } } } private List<Extension> extensionList() throws RoutesMapSyntaxException, RoutesMapLoadException { List<Extension> list = new ArrayList<>(); while (t.ttype != Tokenizer.TK_EOF) { switch (t.ttype) { case TK_USE: { t.next(); if (t.ttype != TK_WORD) { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } uses.add(t.sval); t.next(); break; } case TK_INCLUDE: { t.next(); if (t.ttype != TK_STRCONST) { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } try { RouteMapLoader mapLoader = new RouteMapLoader(resourceRepresenter); list.addAll(mapLoader.load(t.sval)); } catch (RoutesMapLoadException | RoutesMapSyntaxException | FileNotFoundException e) { throw new RoutesMapLoadException(t, "Unable to include map: " + t.sval, e); } t.next(); break; } case TK_METHOD: case TK_PATTERN: case TK_WORD: case TK_HOST: case '{': { list.add(extension()); break; } case '}': { t.next(); return list; } default: { throw new RoutesMapSyntaxException(t, "Unexpected token: " + (t.ttype >= 0 ? (char) t.ttype : t.sval)); } } } return list; } public List<Extension> load(String path) throws RoutesMapLoadException, RoutesMapSyntaxException, FileNotFoundException { return load(resourceRepresenter.load(path)); } public List<Extension> loadFromClasspath(String path) throws RoutesMapLoadException, RoutesMapSyntaxException, FileNotFoundException { return load(resourceRepresenter.loadFromClasspath(path)); } public List<Extension> loadFromString(String input) throws NullPointerException, RoutesMapLoadException, RoutesMapSyntaxException { return load(new ByteArrayInputStream(input.getBytes())); } public List<Extension> load(InputStream input) throws RoutesMapLoadException, RoutesMapSyntaxException { t = new Tokenizer(new InputStreamReader(input)); t.next(); return extensionList(); } public List<Extension> getExtensions() { return extensions; } public static Class getClassTypeBySimpleName(String name) throws ClassNotFoundException { switch (name) { case "String": case "string": return String.class; case "int": case "integer": return int.class; case "long": return long.class; case "short": return short.class; case "byte": return byte.class; case "char": return char.class; case "boolean": case "bool": return boolean.class; case "float": return float.class; case "double": return double.class; default: throw new ClassNotFoundException(name); } } }