Java tutorial
/******************************************************************************* * Copyright 2013 Ivan Shubin http://mindengine.net * * 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 net.mindengine.blogix.web.routes; import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.lang.reflect.Method; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import net.mindengine.blogix.utils.BlogixUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.LineIterator; import org.apache.commons.lang3.tuple.Pair; public class DefaultRoutesParser implements RoutesParser { private static final Pattern PATTERN_SKIP = Pattern.compile("[\\-]+"); private static final char COMMENT = '#'; private static final char SPACE = ' '; private static final String URL_PARAM_REGEX = "([a-zA-Z0-9_\\-\\.]*)"; private static final char URL_PARAM_START = '{'; private static final char URL_PARAM_END = '}'; private static final char METHOD_ARGUMENTS_START = '('; private static final char METHOD_ARGUMENTS_END = ')'; private String[] defaultControllerPackages; private String[] defaultProviderPackages; private ClassLoader[] classLoaders; public DefaultRoutesParser(ClassLoader[] classLoaders, String[] defaultControllerPackages, String[] defaultProviderPackages) { this.classLoaders = classLoaders; this.defaultControllerPackages = defaultControllerPackages; this.defaultProviderPackages = defaultProviderPackages; } @Override public List<Route> parseRoutes(File file) throws IOException { LineIterator it = FileUtils.lineIterator(file, "UTF-8"); List<Route> routes = new LinkedList<Route>(); Route currentRoute = null; while (it.hasNext()) { String line = it.nextLine(); if (line.trim().isEmpty()) { currentRoute = null; } else if (line.startsWith(" ")) { loadModelEntryForRoute(currentRoute, line); } else { currentRoute = parseLine(line.trim()); if (currentRoute != null) { routes.add(currentRoute); } } } return routes; } private void loadModelEntryForRoute(Route currentRoute, String line) { line = line.trim(); int id = line.indexOf(":"); if (id > 0) { String name = line.substring(0, id); String value = line.substring(id + 1).trim(); obtainRouteModel(currentRoute).put(name, convertModelValue(value)); } } private Object convertModelValue(String value) { Pattern DOUBLE_PATTERN = Pattern.compile("(\\+|-)?([0-9]+(\\.[0-9]+))"); Pattern NUMERIC_PATTERN = Pattern.compile("(\\+|-)?([0-9]+)"); if (NUMERIC_PATTERN.matcher(value).matches()) { try { return Long.parseLong(value); } catch (Exception e) { return value; } } else if (DOUBLE_PATTERN.matcher(value).matches()) { try { return Double.parseDouble(value); } catch (Exception e) { return value; } } return value; } private Map<String, Object> obtainRouteModel(Route currentRoute) { if (currentRoute.getModel() == null) { currentRoute.setModel(new HashMap<String, Object>()); } return currentRoute.getModel(); } private Route parseLine(String line) throws IOException { return verified(new LineReader().readLine(line)); } private Route verified(Route route) { if (route != null) { /* * Checking that parameterized routes should always have controllers */ if (route.getController() == null || route.getController().getControllerClass() == null || route.getController().getControllerMethod() == null) { if (route.getUrl().getParameters() != null && route.getUrl().getParameters().size() > 0) { throw new RouteParserException("Controller is not defined for route: " + prettyPrintUrl(route)); } } if (route.getView() == null && !viewLessRouteHasControllerReturningFileType(route)) { throw new RouteParserException("View is not defined for route: " + prettyPrintUrl(route) + "\nNo view is only allowed for controllers with return type File"); } if (route.getUrl().getParameters().size() > 0) { if (route.getProvider() == null || route.getProvider().getProviderClass() == null || route.getProvider().getProviderMethod() == null) { throw new RouteParserException( "Provider is not defined for parameterized route: " + prettyPrintUrl(route)); } for (String param : route.getUrl().getParameters()) { if (!route.getController().getParameters().contains(param)) { throw new RouteParserException("Route url parameter '" + param + "' is not used in controller arguments for route: " + prettyPrintUrl(route)); } } } else { if (route.getProvider() != null) { throw new RouteParserException( "Non-parameterized route " + prettyPrintUrl(route) + " does not need a provider"); } } } return route; } private boolean viewLessRouteHasControllerReturningFileType(Route route) { return route.getController() != null && route.getController().getControllerMethod() != null && route.getController().getControllerMethod().getReturnType().equals(File.class); } private static String prettyPrintUrl(Route route) { return route.getUrl().getOriginalUrl(); } private abstract class State { protected Route route; public State(Route route) { this.route = route; } /** * * @param ch - character in line * @return Next state for line parsing, if null is returned then it means that this line should not be processed anymore */ public abstract State processChar(char ch); public void lineFinished() { } //Chain of states // url [urlparams]* -> controller -> controller args -> view -> provider // \________________________________/ } private class ParsingUrl extends State { public ParsingUrl(Route route) { super(route); route.setUrl(new RouteURL("")); } private StringBuffer url = new StringBuffer(""); private StringBuffer originalUrl = new StringBuffer(""); @Override public State processChar(char ch) { if (ch == COMMENT) { if (alreadyStarted()) { return doneWithNextState(); } else return doneCompletely(); } else if (ch == SPACE) { if (alreadyStarted()) { return doneWithNextState(); } } else { if (alreadyStarted()) { if (ch == URL_PARAM_START) { return new ParsingUrlParam(route, this); } else { append(ch); } } else if (ch == '/') { append(ch); } else throw new RouteParserException("Route url should start with /"); } return this; } private State doneCompletely() { route.getUrl().setUrlPattern(url.toString()); route.getUrl().setOriginalUrl(originalUrl.toString()); return null; } private void append(char ch) { url.append(ch); originalUrl.append(ch); } public void append(String url, String toOriginalUrl) { this.url.append(url); this.originalUrl.append(toOriginalUrl); } private boolean alreadyStarted() { return url.length() > 0; } private State doneWithNextState() { doneCompletely(); return new ParsingController(route); } } private class ParsingUrlParam extends State { private ParsingUrl parsingUrl; public ParsingUrlParam(Route route, ParsingUrl parsingUrl) { super(route); this.parsingUrl = parsingUrl; } StringBuffer param = new StringBuffer(""); @Override public State processChar(char ch) { if (ch != URL_PARAM_END) { param.append(ch); return this; } else { parsingUrl.append(URL_PARAM_REGEX, URL_PARAM_START + param.toString() + URL_PARAM_END); route.getUrl().getParameters().add(param.toString()); return parsingUrl; } } } private class ParsingSkipWord extends State { private State nextState; public ParsingSkipWord(State nextState) { super(null); this.nextState = nextState; } @Override public State processChar(char ch) { if (ch == SPACE) { return nextState; } return this; } } private class ParsingController extends State { private StringBuffer controller = new StringBuffer(""); public ParsingController(Route route) { super(route); route.setController(new ControllerDefinition()); } @Override public State processChar(char ch) { if (ch == '-') { if (alreadyStarted()) { throw new IllegalArgumentException( "Controller for route " + prettyPrintUrl(route) + " is incorrect"); } else { /* * Means that there is no controller for handling this route * Instead just the view tile will be processed without controller processing */ route.setController(null); return new ParsingSkipWord(new ParsingView(route)); } } else if (ch == SPACE) { if (alreadyStarted()) { return nextStateYetUnclearIfArgsAreThere(); } } else if (ch == METHOD_ARGUMENTS_START) { if (alreadyStarted()) { return nextStateParseArgs(); } else throw new RouteParserException( "There is no controller found in route " + prettyPrintUrl(route)); } else { append(ch); } return this; } private State nextStateParseArgs() { processParsedString(); return new ParsingArgs(route).definetelyParseArgs(); } private void append(char ch) { controller.append(ch); } private State nextStateYetUnclearIfArgsAreThere() { processParsedString(); return new ParsingArgs(route); } private void processParsedString() { Pair<Class<?>, Method> pair = BlogixUtils.readClassAndMethodFromParsedString( DefaultRoutesParser.this.classLoaders, controller.toString(), DefaultRoutesParser.this.defaultControllerPackages); route.getController().setControllerClass(pair.getLeft()); route.getController().setControllerMethod(pair.getRight()); if (pair.getRight().getReturnType().equals(Void.TYPE)) { throw new RouteParserException("Controller " + pair.getLeft().getName() + "." + pair.getRight().getName() + " returns void type"); } } private boolean alreadyStarted() { return controller.length() > 0; } } private class ParsingArgs extends State { public ParsingArgs(Route route) { super(route); route.getController().setParameters(new LinkedList<String>()); } private boolean sureIfArgsAreThere = false; private StringBuffer argsBuffer = new StringBuffer(""); public State definetelyParseArgs() { sureIfArgsAreThere = true; return this; } @Override public State processChar(char ch) { if (!sureIfArgsAreThere && !alreadyStarted()) { if (ch == METHOD_ARGUMENTS_START) { sureIfArgsAreThere = true; } else if (ch != SPACE) { return nextStateWithFirstChar(ch); } } if (ch == METHOD_ARGUMENTS_END) { processMethodArguments(); return nextState(); } else if (ch != SPACE && ch != METHOD_ARGUMENTS_START) { argsBuffer.append(ch); } return this; } private void processMethodArguments() { String argsStr = argsBuffer.toString(); if (!argsStr.isEmpty()) { String[] args = argsStr.split(","); for (String arg : args) { if (!arg.isEmpty()) { route.getController().getParameters().add(arg); } } } } private State nextState() { return new ParsingView(route); } private State nextStateWithFirstChar(char ch) { return new ParsingView(route).withAdditionalChar(ch); } private boolean alreadyStarted() { return argsBuffer.length() > 0; } } private abstract class ParsingSimpleString<T> extends State { private StringBuffer stringBuffer = new StringBuffer(""); public ParsingSimpleString(Route route) { super(route); } @SuppressWarnings("unchecked") public T withAdditionalChar(char ch) { stringBuffer.append(ch); return (T) this; } @Override public State processChar(char ch) { if (ch == SPACE) { if (alreadyStarted()) { return doneAndNowSwitchToNextState(); } } else { stringBuffer.append(ch); } return this; } private boolean alreadyStarted() { return stringBuffer.length() > 0; } public String getParsedString() { return stringBuffer.toString(); } public abstract State doneAndNowSwitchToNextState(); } private class ParsingView extends ParsingSimpleString<ParsingView> { public ParsingView(Route route) { super(route); } @Override public State doneAndNowSwitchToNextState() { done(); return new ParsingProvider(route); } private void done() { String view = getParsedString(); if (!view.isEmpty() && !shouldBeSkipped(view)) { route.setView(view); } else { route.setView(null); } } @Override public void lineFinished() { done(); } } private static boolean shouldBeSkipped(String text) { return PATTERN_SKIP.matcher(text).matches(); } private class ParsingProvider extends ParsingSimpleString<ParsingProvider> { public ParsingProvider(Route route) { super(route); } @Override public State doneAndNowSwitchToNextState() { done(); return null; } private void done() { String provider = getParsedString(); if (!provider.isEmpty()) { RouteProviderDefinition rpd = new RouteProviderDefinition(); Pair<Class<?>, Method> pair = BlogixUtils.readClassAndMethodFromParsedString( DefaultRoutesParser.this.classLoaders, provider, DefaultRoutesParser.this.defaultProviderPackages); rpd.setProviderClass(pair.getLeft()); rpd.setProviderMethod(pair.getRight()); Class<?> returnType = pair.getRight().getReturnType(); if (!returnType.equals(Map[].class)) { throw new RouteParserException("Provider " + pair.getLeft().getName() + "." + pair.getRight().getName() + " does not return Map[] type"); } route.setProvider(rpd); } } @Override public void lineFinished() { done(); } } private class LineReader { private Route route = new Route(); private State state = new ParsingUrl(this.route); public Route readLine(String line) throws IOException { Reader reader = new StringReader(line); int r = -1; while (state != null && (r = reader.read()) != -1) { char ch = (char) r; if (ch == COMMENT) { state.lineFinished(); state = null; } else { state = state.processChar(ch); } } if (state != null && r == -1) { state.lineFinished(); } //Checking if the line actually contained route definition if (route.getUrl().getUrlPattern() == null || !route.getUrl().getUrlPattern().trim().startsWith("/")) { return null; } else { return route; } } } }