net.mindengine.blogix.web.routes.DefaultRoutesParser.java Source code

Java tutorial

Introduction

Here is the source code for net.mindengine.blogix.web.routes.DefaultRoutesParser.java

Source

/*******************************************************************************
* 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;
            }
        }
    }

}