/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.maven;

import ninja.build.WatchAndRestartMachine;
import ninja.build.DelayedRestartTrigger;
import ninja.build.RunClassInSeparateJvmMachine;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import ninja.build.ArgumentTokenizer;

import ninja.standalone.NinjaJetty;
import ninja.standalone.AutoStandalone;
import ninja.standalone.Standalone;
import ninja.utils.NinjaConstant;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;

import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;

/**
 * Starts Ninja's SuperDevMode.
 */
@Mojo(name = "run",
        requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
        defaultPhase = LifecyclePhase.NONE,
        threadSafe = true)
public class NinjaRunMojo extends AbstractMojo {

    @Parameter(defaultValue = "${project}", readonly = true)
    protected MavenProject project;

    @Parameter(defaultValue = "${plugin.artifacts}", readonly = true)
    protected List<org.apache.maven.artifact.Artifact> pluginArtifacts;

    /**
     * Skip execution of this plugin.
     */
    @Parameter(property = "ninja.skip", defaultValue="false", required = true)
    protected boolean skip;

    /**
     * Directory containing the build files.
     *
     * For webapps this is usually
     * something like /User/username/workspace/project/target/classes
     */
    @Parameter(property = "ninja.outputDirectory", defaultValue = "${project.build.outputDirectory}", required = true)
    protected String buildOutputDirectory;

    /**
     * All directories to watch for changes.
     */
    @Parameter(property = "ninja.watchDirs", required = false)
    protected File[] watchDirs;

    /**
     * Watch all directories on runtime classpath of project.  For single
     * maven projects this usually will be identical to the buildOutputDirectory and this
     * property is therefore redundant. However, for multi-module projects, all dependent
     * project build output directories will be included (assuming you launched
     * maven from parent project directory). A simple way to use this plugin and
     * watch for changes across an entire multi-module project.
     */
    @Parameter(property = "ninja.watchAllClassPathDirs", defaultValue = "false", required = true)
    protected boolean watchAllClassPathDirs;

    /**
     * Watch all jars on runtime classpath of project. A simple way to monitor
     * all transitive dependencies and trigger a restart if they change.  Since
     * Java's filesystem monitoring API only watches directories, this property
     * adds the parent directory of where the jar is stored and also adds an
     * include rule to always match that jar filename.  If other files in the directory
     * of jars on your classpath (unlikely if you're using maven in a normal manner),
     * then just be cautious you'll want to add exclude rules to not include them.
     */
    @Parameter(property = "ninja.watchAllClassPathJars", defaultValue = "false", required = true)
    protected boolean watchAllClassPathJars;

    /**
     * Includes in Java regex format. Negative regex patterns are difficult to
     * write, so include rules are processed before exclude rules.  Positive
     * matches shortcut the matching process and that file will be included.
     */
    @Parameter(property = "ninja.includes", required = false)
    protected List<String> includes;

    /**
     * Exludes in Java regex format. If you want to exclude all
     * freemarker templates use something like (.*)ftl.html$ for instance.
     */
    @Parameter(property = "ninja.excludes", required = false)
    protected List<String> excludes;

    /**
     * Adds assets directory and freemarker templates to excluded files.
     * These are loaded from the src directory in dev mode.
     *
     * Default excludes are:
     * - "(.*)ftl\\.html$"
     * - "File.pathSeparator + "assets" + File.separator"
     */
    @Parameter(property = "ninja.useDefaultExcludes", defaultValue = "true", required = true)
    protected boolean useDefaultExcludes;

    /**
     * Context path for SuperDevMode.
     */
    @Parameter(property = "ninja.context", required = false)
    protected String context;

    @Deprecated
    protected String contextPath;

    /**
     * Mode for SuperDevMode.
     */
    @Parameter(property = "ninja.mode", defaultValue=NinjaConstant.MODE_DEV, required = false)
    protected String mode;

    /**
     * Port for SuperDevMode (can also be set in conf/application.conf)
     */
    @Parameter(property = "ninja.port", required = false)
    protected Integer port;

    /**
     * SSL port for SuperDevMode (can also be set in conf/application.conf)
     */
    @Parameter(property = "ninja.ssl.port", required = false)
    protected Integer sslPort;

    /**
     * Main class to run in SuperDevMode. Defaults to default standalone class
     * in Ninja.
     */
    @Parameter(property = "ninja.mainClass", required = false)
    protected String mainClass;

    /**
     * Extra arguments to pass to the forked JVM. If you keep your arguments
     * fairly simple, they should be tokenized (split) correctly.  Uses excellent
     * DrJava ArgumentTokenizer class for splitting line into separate arguments.
     */
    @Parameter(property = "ninja.jvmArgs", required = false)
    protected String jvmArgs;

    /**
     * Amount of time to wait for file changes to settle down before triggering a
     * restart in SuperDevMode.
     */
    @Parameter(property = "ninja.settleDownMillis", defaultValue="500", required = false)
    protected Long settleDownMillis;

    @Override
    public void execute() throws MojoExecutionException {

        if (skip) {
            getLog().info("Skip flag is on. Will not execute.");
            return;
        }

        initMojoFromUserSubmittedParameters();

        List<String> classpathItems = new ArrayList<>();

        alertAndStopExecutionIfDirectoryWithCompiledClassesOfThisProjectDoesNotExist(
            buildOutputDirectory);

        classpathItems.add(buildOutputDirectory);

        for (org.apache.maven.artifact.Artifact artifact : project.getArtifacts()) {
            classpathItems.add(artifact.getFile().toString());
        }

        List<Artifact> allArtifactsFromNinjaStandaloneInPlugin
            = getAllArtifactsComingFromNinjaStandalone(pluginArtifacts);

        for (Artifact artifact: allArtifactsFromNinjaStandaloneInPlugin) {

            //only add once...
            if (!classpathItems.contains(artifact.getFile().toString())) {
                classpathItems.add(artifact.getFile().toString());
            }

        }

        Set<String> includesAsSet = new LinkedHashSet<>((includes != null ? includes : Collections.EMPTY_LIST));
        Set<String> excludesAsSet = new LinkedHashSet<>((excludes != null ? excludes : Collections.EMPTY_LIST));

        // start building set of directories to recursively watch
        Set<Path> directoriesToRecursivelyWatch = new LinkedHashSet<>();

        // add only absolute paths so we can catch duplicates

        // add buildOutputDirectory
        directoriesToRecursivelyWatch.add(
            FileSystems.getDefault().getPath(
                buildOutputDirectory).toAbsolutePath());

        // add any watch directories
        if (this.watchDirs != null) {
            for (File watchDir: this.watchDirs) {
                directoriesToRecursivelyWatch.add(watchDir.toPath().toAbsolutePath());
            }
        }

        // add stuff from classpath
        for (org.apache.maven.artifact.Artifact artifact : project.getArtifacts()) {
            File file = artifact.getFile();

            if (file.isDirectory() && this.watchAllClassPathDirs) {
                directoriesToRecursivelyWatch.add(file.toPath().toAbsolutePath());
            }
            else if (file.getName().endsWith(".jar") && this.watchAllClassPathJars) {
                File parentDir = file.getParentFile();
                Path parentPath = parentDir.toPath().toAbsolutePath();

                // safe string for rules below (windows path must be escaped for
                // use in regular expressions
                String rulePrefix = parentDir.getAbsolutePath() + File.separator;
                rulePrefix = rulePrefix.replace("\\", "\\\\");

                // if not previously added then add an exclusion rule for everything in it
                if (!directoriesToRecursivelyWatch.contains(parentPath)) {

                    excludesAsSet.add(rulePrefix + "(.*)$");

                }

                // we also need to add this jar with an inclusion rule so that we always match it
                includesAsSet.add(rulePrefix + file.getName() + "$");

                directoriesToRecursivelyWatch.add(parentPath);
            }
        }

        // which standalone (works with -Dninja.standalone to maven OR in pom.xml)
        String mainClassToRun = (mainClass != null ? mainClass : AutoStandalone.class.getCanonicalName());

        getLog().info("------------------------------------------------------------------------");

        getLog().info("Ninja will watch dirs:");
        for (Path path : directoriesToRecursivelyWatch) {
            getLog().info(" " + path);
        }
        getLog().info("");
        getLog().info("Ninja will launch with:");
        getLog().info("      context: " + (getContextPath() != null ? getContextPath() : "<default>"));
        getLog().info("         mode: " + mode);
        getLog().info("         port: " + (port != null ? port : "<default>"));
        getLog().info("     ssl port: " + (sslPort != null ? sslPort : "<default>"));
        getLog().info("    mainClass: " + mainClassToRun);
        getLog().info("extra jvmArgs: " + (jvmArgs != null ? jvmArgs : "<none>"));
        getLog().info("------------------------------------------------------------------------");

        try {
            //
            // build dependencies, start them, and then watch
            //

            RunClassInSeparateJvmMachine machine = buildRunClassInSeparateJvmMachine(
                "Standalone",
                mainClassToRun,
                classpathItems,
                buildJvmArguments(),
                project.getBasedir()
            );

            DelayedRestartTrigger restartTrigger = buildDelayedRestartTrigger(machine);
            restartTrigger.setSettleDownMillis(settleDownMillis);

            restartTrigger.start();

            WatchAndRestartMachine watcher = buildWatchAndRestartMachine(
                directoriesToRecursivelyWatch,
                includesAsSet,
                excludesAsSet,
                restartTrigger);

            // initial startup of machine
            machine.restart();

            watcher.run();

        } catch (IOException e) {
            getLog().error(e);
        }
    }

    protected String getContextPath() {
        if (this.context != null) {
            return this.context;
        } else {
            return this.contextPath;
        }
    }

    // so we can mock a fake one for unit testing
    protected DelayedRestartTrigger buildDelayedRestartTrigger(RunClassInSeparateJvmMachine machine) {
        return new DelayedRestartTrigger(machine);
    }

    protected WatchAndRestartMachine buildWatchAndRestartMachine(
            Set<Path> directoriesToRecursivelyWatch,
            Set<String> includes,
            Set<String> excludes,
            DelayedRestartTrigger restartTrigger) throws IOException {
        return new WatchAndRestartMachine(
                directoriesToRecursivelyWatch,
                includes,
                excludes,
                restartTrigger);
    }

    // so we can mock a fake one for unit testing
    protected RunClassInSeparateJvmMachine buildRunClassInSeparateJvmMachine(
            String name,
            String classNameWithMainToRun,
            List<String> classpath,
            List<String> jvmArguments,
            File mavenBaseDir) {
        return new RunClassInSeparateJvmMachine(
            name,
            classNameWithMainToRun,
            classpath,
            buildJvmArguments(),
            mavenBaseDir
        );
    }

    protected List<String> buildJvmArguments() {
        List<String> jvmArguments = new ArrayList<>();

        String systemPropertyDevMode
                = "-D" + NinjaConstant.MODE_KEY_NAME + "=" + mode;

        jvmArguments.add(systemPropertyDevMode);

        if (port != null) {
            String portSelection
                    = "-D" + Standalone.KEY_NINJA_PORT + "=" + port;
            jvmArguments.add(portSelection);
        }

        if (sslPort != null) {
            String sslPortSelection
                    = "-D" + Standalone.KEY_NINJA_SSL_PORT + "=" + sslPort;
            jvmArguments.add(sslPortSelection);
        }

        if (getContextPath() != null) {
            String contextPathSelection
                    = "-D" + Standalone.KEY_NINJA_CONTEXT_PATH + "=" + getContextPath();
            jvmArguments.add(contextPathSelection);
        }

        if (jvmArgs != null) {
            // use excellent DrJava library to tokenize arguments (do not keep tokens)
            List<String> tokenizedArgs = ArgumentTokenizer.tokenize(jvmArgs, false);
            getLog().debug("JVM arguments tokenizer results:");
            for (String s : tokenizedArgs) {
                getLog().debug("argument: " + s + "");
            }
            jvmArguments.addAll(tokenizedArgs);
        }

        return jvmArguments;
    }

    protected void initMojoFromUserSubmittedParameters() {

        if (useDefaultExcludes) {

            excludes.addAll(
                Arrays.asList(
                    NinjaMavenPluginConstants.DEFAULT_EXCLUDE_PATTERNS));

        }

    }

    /**
     * This might be a bit hacky. But just a little bit.
     *
     * We have to add ninja-standalone and all of its dependencies to
     * the classpath, so that NinjaJetty can start.
     *
     * But we do not want the user to declare the dependencies inside his
     * project pom.xml.
     *
     * Therefore we declare the dependency in this plugin pom.xml.
     * But we do not want to include all of this plugin's dependencies in the
     * classpath for NinjaJetty. Therefore this method filters everything and
     * only adds  dependencies (also transitive deps) that originate
     * from ninja-standalone. That way we get all deps we need.
     *
     * @param artifacts A list of artifacts that will be filtered.
     * @return All artifacts coming from artifactId "ninja-standalone"
     *         (including transitive dependencies)
     */
    protected List<Artifact> getAllArtifactsComingFromNinjaStandalone(
        List<Artifact> artifacts) {

        List<Artifact> resultingArtifacts = new ArrayList<>();

        for (Artifact artifact: artifacts) {

            for (String dependencyTrail: artifact.getDependencyTrail()) {

                // something like:  org.ninjaframework:ninja-standalone:jar:2.5.2
                if (dependencyTrail.contains(NinjaMavenPluginConstants.NINJA_STANDALONE_ARTIFACT_ID)) {

                    resultingArtifacts.add(artifact);
                    break;

                }

            }

        }

        return resultingArtifacts;

    }

    protected void alertAndStopExecutionIfDirectoryWithCompiledClassesOfThisProjectDoesNotExist(
        String directoryWithCompiledClassesOfThisProject) throws MojoExecutionException {

        File classesDir = null;

        if (directoryWithCompiledClassesOfThisProject != null) {
            classesDir = new File(directoryWithCompiledClassesOfThisProject);
        }

        if (classesDir == null || !classesDir.exists()) {

            getLog().error("Directory with classes does not exist: " + directoryWithCompiledClassesOfThisProject);
            getLog().error("Maybe running 'mvn compile'  before running 'mvn ninja:run' helps :)");

            // BAM!
            //System.exit(1);
            throw new MojoExecutionException("Directory with classes does not exist: " + directoryWithCompiledClassesOfThisProject);
        }

    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.standalone;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URL;
import java.security.KeyStore;
import java.util.Iterator;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import ninja.utils.ForwardingServiceLoader;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper utilities for working with standalone applications.
 */
public class StandaloneHelper {
    static private final Logger log = LoggerFactory.getLogger(StandaloneHelper.class);

    static private final String URI_SCHEME_CLASSPATH = "classpath";

    static public int findAvailablePort(int min, int max) {
        for (int port = min; port < max; port++) {
            try {
                new ServerSocket(port).close();
                return port;
            } catch (IOException e) {
                // Must already be taken
            }
        }
        throw new IllegalStateException(
                "Could not find available port in range " + min + " to " + max);
    }

    /**
     * Resolves which standalone class to use. Either defined as a system
     * property or falling back to the default.
     * @return The resolved standalone class to use
     */
    static public Class<? extends Standalone> resolveStandaloneClass() {
        return resolveStandaloneClass(
            System.getProperty(Standalone.KEY_NINJA_STANDALONE_CLASS),
            ForwardingServiceLoader.loadWithSystemServiceLoader(Standalone.class),
            Standalone.DEFAULT_STANDALONE_CLASS
        );
    }

    // used for testing (so we can inject mocks of system static methods)
    static Class<? extends Standalone> resolveStandaloneClass(String standaloneClassNameSystemProperty,
                                                              ForwardingServiceLoader<Standalone> standaloneServiceLoader,
                                                              String standaloneClassNameDefaultValue) {
        Class<? extends Standalone> resolvedStandaloneClass = null;

        // 1. System property for 'ninja.standalone.class'
        if (standaloneClassNameSystemProperty != null) {
            try {
                resolvedStandaloneClass
                    = (Class<Standalone>)Class.forName(standaloneClassNameSystemProperty);
            } catch (Exception e) {
                throw new RuntimeException("Unable to find standalone class '" + standaloneClassNameSystemProperty + "' (class does not exist)");
            }
        }

        // 2. Implementation on classpath that's registered as service?
        if (resolvedStandaloneClass == null) {
            try {
                Iterator<Standalone> standaloneIterator = standaloneServiceLoader.iterator();
                // first one wins
                if (standaloneIterator.hasNext()) {
                    resolvedStandaloneClass = standaloneIterator.next().getClass();
                }
                // more than one triggers warning
                if (standaloneIterator.hasNext()) {
                    log.warn("More than one implementation of {} on classpath! Using {} which was the first", Standalone.class, resolvedStandaloneClass);
                }
            } finally {
                // always kill cache (since ServiceLoader actually instantiates an instance)
                standaloneServiceLoader.reload();
            }
        }

        // 3. Fallback to ninja default
        if (resolvedStandaloneClass == null) {
            try {
                resolvedStandaloneClass
                    = (Class<Standalone>)Class.forName(standaloneClassNameDefaultValue);
            } catch (Exception e) {
                throw new RuntimeException("Unable to find standalone class '" + standaloneClassNameDefaultValue + "' (class does not exist)");
            }
        }

        return resolvedStandaloneClass;
    }

    static public Standalone create(Class<? extends Standalone> standaloneClass) {
        try {
            return standaloneClass.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Unable to create " + standaloneClass.getCanonicalName() + " (either not on classpath or invalid class name)");
        }
    }

    static void checkContextPath(String contextPath) {
        // per servlet docs
        // The path starts with a / character but does not end with a / character.
        // For servlets in the default (root) context, this method returns ""
        if (StringUtils.isEmpty(contextPath)) {
            return;
        }

        if (!contextPath.startsWith("/")) {
            throw new IllegalArgumentException("A context path must start with a '/' character. " +
                    " See https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html#getContextPath() for more info");
        }

        if (contextPath.endsWith("/")) {
            throw new IllegalArgumentException("A context path must not end with a '/' character. " +
                    " See https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html#getContextPath() for more info");
        }
    }

    static public InputStream openKeyStoreInput(URI uri) throws IOException {
        if (uri.getScheme().equals(URI_SCHEME_CLASSPATH)) {
            String resourceName = uri.getPath();

            log.debug("Opening keystore on classpath with resource {}", resourceName);

            InputStream stream = StandaloneHelper.class.getResourceAsStream(resourceName);

            if (stream == null) {
                throw new IOException("Resource '" + resourceName + "' not found on classpath");
            }

            return stream;
        } else {
            URL url = uri.toURL();

            log.debug("Opening keystore with url {}", url);

            return url.openStream();
        }
    }

    static public KeyStore loadKeyStore(URI uri, char[] password) throws Exception {
        try (InputStream stream = openKeyStoreInput(uri)) {
            KeyStore ks = KeyStore.getInstance("JKS");
            ks.load(stream, password);
            return ks;
        }
    }

    static public SSLContext createSSLContext(
            URI keystoreUri, char[] keystorePassword,
            URI truststoreUri, char[] truststorePassword) throws Exception {

        // load keystore
        KeyStore keystore = loadKeyStore(keystoreUri, keystorePassword);
        KeyManager[] keyManagers;
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keystore, keystorePassword);
        keyManagers = keyManagerFactory.getKeyManagers();

        // load truststore
        KeyStore truststore = loadKeyStore(truststoreUri, truststorePassword);
        TrustManager[] trustManagers;
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(truststore);
        trustManagers = trustManagerFactory.getTrustManagers();

        SSLContext sslContext;
        sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagers, trustManagers, null);

        return sslContext;
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.util.Objects;
import java.util.Optional;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * This class encrypts/decrypts session cookie data. Resultant encrypted strings are encoded in base64, and decryption
 * expects base64 encoded string.
 */
@Singleton
public class CookieEncryption {

    public static final String ALGORITHM = "AES";

    private static final Logger logger = LoggerFactory.getLogger(CookieEncryption.class);

    private final Optional<SecretKeySpec> secretKeySpec;

    @Inject
    public CookieEncryption(NinjaProperties properties) {

        Optional<SecretKeySpec> secretKeySpec = Optional.empty();

        if (properties.getBooleanWithDefault(NinjaConstant.applicationCookieEncrypted, false)) {

            String secret = properties.getOrDie(NinjaConstant.applicationSecret);
            try {
                int maxKeyLengthBits = Cipher.getMaxAllowedKeyLength(ALGORITHM);
                if (maxKeyLengthBits == Integer.MAX_VALUE) {
                    maxKeyLengthBits = 256;
                }

                secretKeySpec = Optional.of(
                        new SecretKeySpec(secret.getBytes(), 0, maxKeyLengthBits / Byte.SIZE, ALGORITHM));

                logger.info("Ninja session encryption is using {} / {} bit.", secretKeySpec.get().getAlgorithm(), maxKeyLengthBits);

            } catch (Exception exception) {
                logger.error("Can not create class to encrypt cookie.", exception);
                throw new RuntimeException(exception);
            }

        }

        this.secretKeySpec = secretKeySpec;

    }

    /**
     * Encrypts data with secret key.
     *
     * @param data text to encrypt
     * @return encrypted text in base64 format
     */
    public String encrypt(String data) {

        Objects.requireNonNull(data, "Data to be encrypted");

        if (!secretKeySpec.isPresent()) {
            return data;
        }

        try {
            // encrypt data
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec.get());
            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

            // convert encrypted bytes to string in base64
            return Base64.encodeBase64URLSafeString(encrypted);

        } catch (InvalidKeyException ex) {
            logger.error(getHelperLogMessage(), ex);
            throw new RuntimeException(ex);
        } catch (GeneralSecurityException ex) {
            logger.error("Failed to encrypt data.", ex);
            return "";
        }
    }

    /**
     * Decrypts data with secret key.
     *
     * @param data text to decrypt in base64 format
     * @return decrypted text
     */
    public String decrypt(String data) {

        Objects.requireNonNull(data, "Data to be decrypted");

        if (!secretKeySpec.isPresent()) {
            return data;
        }

        // convert base64 encoded string to bytes
        byte[] decoded = Base64.decodeBase64(data);
        try {
            // decrypt bytes
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec.get());
            byte[] decrypted = cipher.doFinal(decoded);

            // convert bytes to string
            return new String(decrypted, StandardCharsets.UTF_8);

        } catch (InvalidKeyException ex) {
            logger.error(getHelperLogMessage(), ex);
            throw new RuntimeException(ex);
        } catch (GeneralSecurityException ex) {
            logger.error("Failed to decrypt data.", ex);
            return "";
        }
    }

    private String getHelperLogMessage() {
        StringBuilder sb = new StringBuilder();
        sb.append("Invalid key provided. Check if application secret is properly set.").append(System.lineSeparator());
        sb.append("You can remove '").append(NinjaConstant.applicationSecret).append("' key in configuration file ");
        sb.append("and restart application. Ninja will generate new key for you.");
        return sb.toString();
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.io.InputStream;
import java.util.Optional;
import java.util.Properties;

import javax.management.RuntimeErrorException;

import ninja.diagnostics.DiagnosticError;
import ninja.diagnostics.DiagnosticErrorBuilder;
import ninja.exceptions.BadRequestException;
import ninja.exceptions.ForbiddenRequestException;
import ninja.exceptions.RenderingException;
import ninja.exceptions.RequestNotFoundException;
import ninja.i18n.Messages;
import ninja.lifecycle.LifecycleService;
import ninja.utils.Message;
import ninja.utils.NinjaConstant;
import ninja.utils.NinjaProperties;
import ninja.utils.ResultHandler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;

public class NinjaDefault implements Ninja {
    private static final Logger logger = LoggerFactory.getLogger(NinjaDefault.class);

    /**
     * The most important thing: A cool logo.
     */
    private final String NINJA_LOGO = "\n"
            + " _______  .___ _______        ____.  _____   \n"
            + " \\      \\ |   |\\      \\      |    | /  _  \\  \n"
            + " /   |   \\|   |/   |   \\     |    |/  /_\\  \\ \n"
            + "/    |    \\   /    |    \\/\\__|    /    |    \\  http://www.ninjaframework.org\n"
            + "\\____|__  /___\\____|__  /\\________\\____|__  /  @ninjaframework\n"
            + "     web\\/framework   \\/                  \\/   {}\n";

    @Inject
    protected LifecycleService lifecycleService;

    @Inject
    protected Router router;

    @Inject
    protected ResultHandler resultHandler;

    @Inject
    Messages messages;

    @Inject
    NinjaProperties ninjaProperties;

    /**
     * Whether diagnostics are enabled. If enabled then the default system/views
     * will be skipped and a detailed diagnostic error result will be returned
     * by the various methods in this class. You get precise feedback where
     * an error occurred including original source code.
     *
     * @return True if diagnostics are enabled otherwise false.
     */
    public boolean isDiagnosticsEnabled() {
        // extra safety: only disable detailed diagnostic error pages
        // if both in DEV mode and diagnostics are enabled 0
        return ninjaProperties.isDev() && ninjaProperties.getBooleanWithDefault(NinjaConstant.DIAGNOSTICS_KEY_NAME, Boolean.TRUE);
    }

    @Override
    public void onRouteRequest(Context.Impl context) {

        String httpMethod = context.getMethod();

        Route route = router.getRouteFor(httpMethod, context.getRequestPath());

        context.setRoute(route);

        if (route != null) {

            Result underlyingResult = null;

            try {

                underlyingResult = route.getFilterChain().next(context);

                resultHandler.handleResult(underlyingResult, context);

            } catch (Exception exception) {

                // call special handler to capture the underlying result if there is one
                Result result = onException(context, exception, underlyingResult);
                renderErrorResultAndCatchAndLogExceptions(result, context);

            } finally {

                context.cleanup();

            }

        } else {

            // throw a 404 "not found" because we did not find the route
            Result result = getNotFoundResult(context);
            renderErrorResultAndCatchAndLogExceptions(result, context);

        }

    }

    @Override
    public void renderErrorResultAndCatchAndLogExceptions(
            Result result, Context context) {

        try {
            if (context.isAsync()) {
                context.returnResultAsync(result);
            } else {
                resultHandler.handleResult(result, context);
            }
        } catch (Exception exceptionCausingRenderError) {
            logger.error("Unable to handle result. That's really really fishy.",
                    exceptionCausingRenderError);
        }
    }

    @Override
    public void onFrameworkStart() {

        showSplashScreenViaLogger();

        lifecycleService.start();
    }

    @Override
    public void onFrameworkShutdown() {
        lifecycleService.stop();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Results for exceptions (404, 500 etc)
    ////////////////////////////////////////////////////////////////////////////

    @Override
    public Result onException(Context context, Exception exception) {

        return onException(context, exception, null);

    }

    public Result onException(Context context, Exception exception, Result underlyingResult) {

        Result result;

        // log the exception as debug
        logger.debug("Unable to process request", exception);

        if (exception instanceof BadRequestException) {

            result = getBadRequestResult(context, (BadRequestException) exception);

        } else if (exception instanceof ForbiddenRequestException) {

            result = getForbiddenResult(context, (ForbiddenRequestException) exception);

        } else if (exception instanceof RequestNotFoundException) {

            result = getNotFoundResult(context, (RequestNotFoundException) exception);

        } else if (exception instanceof RenderingException) {

            result = getRenderingExceptionResult(context, (RenderingException) exception, underlyingResult);

        } else {

            result = getInternalServerErrorResult(context, exception, underlyingResult);

        }

        return result;

    }

    public Result getRenderingExceptionResult(Context context, RenderingException exception, Result underlyingResult) {

        if (isDiagnosticsEnabled()) {

            // prefer provided title and underlying cause
            DiagnosticError diagnosticError = DiagnosticErrorBuilder
                .buildDiagnosticError(
                    (exception.getTitle() == null ? "Rendering exception" : exception.getTitle()),
                    (exception.getCause() == null ? exception : exception.getCause()),
                    exception.getSourcePath(),
                    exception.getLineNumber(),
                    underlyingResult);

            return Results.internalServerError().render(diagnosticError);

        }

        return getInternalServerErrorResult(context, exception, underlyingResult);

    }

    /**
     * Deprecated. Check {@link Ninja#getInternalServerErrorResult(Context, Exception, Result)}.
     */
    @Deprecated
    public Result getInternalServerErrorResult(Context context, Exception exception) {
        return getInternalServerErrorResult(context, exception, null);
    }

    @Override
    public Result getInternalServerErrorResult(Context context, Exception exception, Result underlyingResult) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                DiagnosticErrorBuilder.build500InternalServerErrorDiagnosticError(exception, true, underlyingResult);

            return Results.internalServerError().render(diagnosticError);

        }

        logger.error(
                "Emitting bad request 500. Something really wrong when calling route: {} (class: {} method: {})",
                context.getRequestPath(),
                context.getRoute().getControllerClass(),
                context.getRoute().getControllerMethod(),
                exception);

        Message message = buildErrorMessage(
                context,
                NinjaConstant.I18N_NINJA_SYSTEM_INTERNAL_SERVER_ERROR_TEXT_KEY,
                NinjaConstant.I18N_NINJA_SYSTEM_INTERNAL_SERVER_ERROR_TEXT_DEFAULT,
                Optional.ofNullable(exception),
                Optional.<Result>empty());

        Result result = Results
                .internalServerError()
                .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                .fallbackContentType(Result.TEXT_HTML)
                .render(message)
                .template(
                        ninjaProperties.getWithDefault(
                                NinjaConstant.LOCATION_VIEW_HTML_INTERNAL_SERVER_ERROR_KEY,
                                NinjaConstant.LOCATION_VIEW_FTL_HTML_INTERNAL_SERVER_ERROR));

        return result;
    }

    @Override
    public Result getNotFoundResult(Context context) {
        return getNotFoundResult(context, null);
    }

    @Override
    public Result getNotFoundResult(Context context, RequestNotFoundException exception) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                    exception != null
                    ? DiagnosticErrorBuilder.build404NotFoundDiagnosticError(exception, true)
                    : DiagnosticErrorBuilder.build404NotFoundDiagnosticError(true);

            return Results.notFound().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context,
                NinjaConstant.I18N_NINJA_SYSTEM_NOT_FOUND_TEXT_KEY,
                NinjaConstant.I18N_NINJA_SYSTEM_NOT_FOUND_TEXT_DEFAULT,
                Optional.ofNullable(exception),
                Optional.<Result>empty());

        Result result = Results
                        .notFound()
                        .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                        .fallbackContentType(Result.TEXT_HTML)
                        .render(message)
                        .template(
                                ninjaProperties.getWithDefault(
                                        NinjaConstant.LOCATION_VIEW_HTML_NOT_FOUND_KEY,
                                        NinjaConstant.LOCATION_VIEW_FTL_HTML_NOT_FOUND));

        return result;

    }

    @Override
    public Result getBadRequestResult(Context context, BadRequestException exception) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                DiagnosticErrorBuilder.build400BadRequestDiagnosticError(exception, true);

            return Results.badRequest().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context,
                NinjaConstant.I18N_NINJA_SYSTEM_BAD_REQUEST_TEXT_KEY,
                NinjaConstant.I18N_NINJA_SYSTEM_BAD_REQUEST_TEXT_DEFAULT,
                Optional.ofNullable(exception),
                Optional.<Result>empty());

        Result result = Results
                        .badRequest()
                        .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                        .fallbackContentType(Result.TEXT_HTML)
                        .render(message)
                        .template(
                                ninjaProperties.getWithDefault(
                                        NinjaConstant.LOCATION_VIEW_HTML_BAD_REQUEST_KEY,
                                        NinjaConstant.LOCATION_VIEW_FTL_HTML_BAD_REQUEST));

        return result;

    }

    @Override
    public Result getUnauthorizedResult(Context context) {

        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                DiagnosticErrorBuilder.build401UnauthorizedDiagnosticError();

            return Results.unauthorized().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context,
                NinjaConstant.I18N_NINJA_SYSTEM_UNAUTHORIZED_REQUEST_TEXT_KEY,
                NinjaConstant.I18N_NINJA_SYSTEM_UNAUTHORIZED_REQUEST_TEXT_DEFAULT,
                Optional.<Throwable>empty(),
                Optional.<Result>empty());

        // WWW-Authenticate must be included per the spec
        // http://www.ietf.org/rfc/rfc2617.txt 3.2.1 The WWW-Authenticate Response Header
        Result result = Results
                        .unauthorized()
                        .addHeader(Result.WWW_AUTHENTICATE, "None")
                        .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                        .fallbackContentType(Result.TEXT_HTML)
                        .render(message)
                        .template(
                                ninjaProperties.getWithDefault(
                                        NinjaConstant.LOCATION_VIEW_HTML_UNAUTHORIZED_KEY,
                                        NinjaConstant.LOCATION_VIEW_FTL_HTML_UNAUTHORIZED));

        return result;

    }

    @Override
    public Result getForbiddenResult(Context context) {
        return getForbiddenResult(context, null);
    }

    @Override
    public Result getForbiddenResult(Context context, ForbiddenRequestException exception) {

        // diagnostic mode
        if (isDiagnosticsEnabled()) {

            DiagnosticError diagnosticError =
                exception != null
                ? DiagnosticErrorBuilder.build403ForbiddenDiagnosticError(exception, true)
                : DiagnosticErrorBuilder.build403ForbiddenDiagnosticError();

            return Results.forbidden().render(diagnosticError);

        }

        Message message = buildErrorMessage(
                context,
                NinjaConstant.I18N_NINJA_SYSTEM_FORBIDDEN_REQUEST_TEXT_KEY,
                NinjaConstant.I18N_NINJA_SYSTEM_FORBIDDEN_REQUEST_TEXT_DEFAULT,
                Optional.ofNullable(exception),
                Optional.<Result>empty());

        Result result = Results
                        .forbidden()
                        .supportedContentTypes(Result.TEXT_HTML, Result.APPLICATION_JSON, Result.APPLICATION_XML)
                        .fallbackContentType(Result.TEXT_HTML)
                        .render(message)
                        .template(
                                ninjaProperties.getWithDefault(
                                        NinjaConstant.LOCATION_VIEW_HTML_FORBIDDEN_KEY,
                                        NinjaConstant.LOCATION_VIEW_FTL_HTML_FORBIDDEN));

        return result;

    }

    protected Message buildErrorMessage(Context context,
                                        String errorTextKey,
                                        String errorTextDefault,
                                        Optional<Throwable> exception,
                                        Optional<Result> underlyingResult) {

        String messageI18n
                = messages.getWithDefault(
                        errorTextKey,
                        errorTextDefault,
                        context,
                        underlyingResult);

        String errorI18n
                = !exception.isPresent()
                    ? null
                    : messages.getWithDefault(
                        exception.get().getMessage(),
                        exception.get().getLocalizedMessage(),
                        context,
                        underlyingResult);

        return new Message(messageI18n, errorI18n);
    }

    /**
     * Simply reads a property resource file that contains the version of this
     * Ninja build. Helps to identify the Ninja version currently running.
     *
     * @return The version of Ninja. Eg. "1.6-SNAPSHOT" while developing of "1.6" when released.
     */
    private final String readNinjaVersion() {

        // location of the properties file
        String LOCATION_OF_NINJA_BUILTIN_PROPERTIES = "ninja/ninja-builtin.properties";
        // and the key inside the properties file.
        String NINJA_VERSION_PROPERTY_KEY = "ninja.version";

        String ninjaVersion;
        try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(LOCATION_OF_NINJA_BUILTIN_PROPERTIES)){

            Properties prop = new Properties();
            prop.load(stream);

            ninjaVersion = prop.getProperty(NINJA_VERSION_PROPERTY_KEY);

        } catch (Exception e) {
            //this should not happen. Never.
            throw new RuntimeErrorException(new Error("Something is wrong with your build. Cannot find resource " + LOCATION_OF_NINJA_BUILTIN_PROPERTIES));
        }

        return ninjaVersion;

    }

    private final void showSplashScreenViaLogger() {

        String ninjaVersion = readNinjaVersion();

        // log Ninja splash screen
        logger.info(NINJA_LOGO, ninjaVersion);

    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.build;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.nio.file.SensitivityWatchEventModifier;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public class WatchAndRestartMachine implements Runnable {
    private static final Logger log = LoggerFactory.getLogger(WatchAndRestartMachine.class);

    private boolean shutdown;
    private final DelayedRestartTrigger restartTrigger;
    private final Set<String> includes;
    private final Set<String> excludes;
    private final WatchService watchService;
    private final Map<WatchKey, Path> mapOfWatchKeysToPaths;
    private final AtomicInteger takeCount;

    public WatchAndRestartMachine(
            Path directoryToRecursivelyWatch,
            Set<String> includes,
            Set<String> excludes,
            DelayedRestartTrigger restartTrigger) throws IOException {

        this(new HashSet<>(Arrays.asList(directoryToRecursivelyWatch)),
                includes, excludes, restartTrigger);

    }

    public WatchAndRestartMachine(
            Set<Path> directoriesToRecursivelyWatch,
            Set<String> includes,
            Set<String> excludes,
            DelayedRestartTrigger restartTrigger) throws IOException {

        this.watchService = FileSystems.getDefault().newWatchService();
        this.mapOfWatchKeysToPaths = new HashMap<>();
        this.includes = includes;
        this.excludes = excludes;
        this.restartTrigger = restartTrigger;
        this.takeCount = new AtomicInteger(0);
        for (Path path: directoriesToRecursivelyWatch) {
            registerAll(path);
        }

    }

    public void shutdown() {
        this.shutdown = true;
        // doesn't do anything but does prevent logging an error
    }

    private void registerAll(final Path path) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path path,
                    BasicFileAttributes attrs)
                    throws IOException {
                register(path);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * Register the given path with the WatchService
     */
    private void register(Path path) throws IOException {
        ////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //// USUALLY THIS IS THE DEFAULT WAY TO REGISTER THE EVENTS:
        ////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //        WatchKey watchKey = path.register(
        //                watchService,
        //                ENTRY_CREATE,
        //                ENTRY_DELETE,
        //                ENTRY_MODIFY);

        ////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //// BUT THIS IS DAMN SLOW (at least on a Mac)
        //// THEREFORE WE USE EVENTS FROM COM.SUN PACKAGES THAT ARE WAY FASTER
        //// THIS MIGHT BREAK COMPATIBILITY WITH OTHER JDKs
        //// MORE: http://stackoverflow.com/questions/9588737/is-java-7-watchservice-slow-for-anyone-else
        ////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        WatchKey watchKey = path.register(
            watchService,
            new WatchEvent.Kind[]{
                StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY,
                StandardWatchEventKinds.ENTRY_DELETE
            },
            SensitivityWatchEventModifier.HIGH);

        mapOfWatchKeysToPaths.put(watchKey, path);
    }

    @Override
    public void run() {

        for (;;) {

            WatchKey watchKey;
            try {
                watchKey = watchService.take();
                takeCount.incrementAndGet();
            } catch (InterruptedException e) {
                if (!shutdown) {
                    log.error("Unexpectedly interrupted while waiting for take()", e);
                }
                return;
            }

            Path path = mapOfWatchKeysToPaths.get(watchKey);
            if (path == null) {
                log.error("WatchKey not recognized!!");
                continue;
            }

            for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
                WatchEvent.Kind watchEventKind = watchEvent.kind();

                // TBD - provide example of how OVERFLOW watchEvent is handled
                if (watchEventKind == OVERFLOW) {
                    continue;
                }

                // Context for directory entry watchEvent is the file name of entry
                WatchEvent<Path> ev = (WatchEvent<Path>) watchEvent;
                Path name = ev.context();
                Path child = path.resolve(name);

                if (watchEventKind == ENTRY_MODIFY) {
                    // we are not interested in events from parent directories...
                    if (!child.toFile().isDirectory()) {
                        handleNewOrModifiedFile("Modified", child);
                    }
                }

                // if directory is created, then register it and its sub-directories recursively
                if (watchEventKind == ENTRY_CREATE) {

                    if (!child.toFile().isDirectory()) {
                        handleNewOrModifiedFile("New", child);
                    }
                    try {
                        if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
                            registerAll(child);
                        }
                    } catch (IOException e) {
                        log.error("Something fishy happened. Unable to register new dir for watching", e);
                    }

                }

            }

            // reset watchKey and remove from set if directory no longer accessible
            boolean valid = watchKey.reset();
            if (!valid) {
                mapOfWatchKeysToPaths.remove(watchKey);

                // all directories are inaccessible
                if (mapOfWatchKeysToPaths.isEmpty()) {
                    break;
                }

            }

        }

    }

    public void handleNewOrModifiedFile(String newOrMod, Path path) {
        String f = path.toFile().getAbsolutePath();

        log.debug("{} file detected: {}", newOrMod, f);

        RuleMatch match = matchRule(includes, excludes, f);

        log.debug(" matched rule: type={}, pattern={}, proceed={}", match.type, match.pattern, match.proceed);

        if (match.proceed) {
            log.debug(" will trigger restart", newOrMod, f);
            restartTrigger.trigger();
        }
        else {
            log.debug(" will not trigger restart");
        }
    }

    public static enum RuleType {
        none,
        include,
        exclude
    }

    public static class RuleMatch {

        final RuleType type;
        final String pattern;
        final boolean proceed;

        public RuleMatch(RuleType type, String pattern, boolean proceed) {
            this.type = type;
            this.pattern = pattern;
            this.proceed = proceed;
        }

    }

    public static RuleMatch matchRule(Set<String> includes, Set<String> excludes, String string) {

        if (includes != null) {
            for (String regex: includes) {
                if (string.matches(regex)) {
                    return new RuleMatch(RuleType.include, regex, true);
                }
            }
        }

        if (excludes != null) {
            for (String regex: excludes) {
                if (string.matches(regex)) {
                    return new RuleMatch(RuleType.exclude, regex, false);
                }
            }
        }

        return new RuleMatch(RuleType.none, "", true);
    }

    public static boolean checkIfWouldBeExcluded(Set<String> patterns, String string) {
        // use "excludes" above to use this for testing
        return !matchRule(null, patterns, string).proceed;
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.collect.Maps;
import java.util.Iterator;
import org.apache.commons.lang.StringUtils;

/**
 * A route
 */
public class Route {

    static public final String HTTP_METHOD_GET = "GET";
    static public final String HTTP_METHOD_POST = "POST";
    static public final String HTTP_METHOD_PUT = "PUT";
    static public final String HTTP_METHOD_HEAD = "HEAD";
    static public final String HTTP_METHOD_DELETE = "DELETE";
    static public final String HTTP_METHOD_OPTIONS = "OPTIONS";
    static public final String HTTP_METHOD_WEBSOCKET = "WS";

    //Matches: {id} AND {id: .*?}
    // group(1) extracts the name of the group (in that case "id").
    // group(3) extracts the regex if defined
    final static Pattern PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE
        = Pattern.compile("\\{(.*?)(:\\s(.*?))?\\}");

    /**
     * This regex matches everything in between path slashes.
     */
    final static String VARIABLE_ROUTES_DEFAULT_REGEX = "([^/]*)";

    private final String httpMethod;
    private final String uri;
    private final Method controllerMethod;
    private final FilterChain filterChain;
    private final Map<String,RouteParameter> parameters;
    private final Pattern regex;

    public Route(String httpMethod,
            String uri,
            Method controllerMethod,
            FilterChain filterChain) {
        this.httpMethod = httpMethod;
        this.uri = uri;
        this.controllerMethod = controllerMethod;
        this.filterChain = filterChain;
        this.parameters = RouteParameter.parse(uri);
        this.regex = Pattern.compile(convertRawUriToRegex(uri));
    }

    /**
     * @deprecated Use getUri()
     */
    public String getUrl() {
        return uri;
    }

    public String getHttpMethod() {
        return httpMethod;
    }

    public boolean isHttpMethod(String method) {
        return StringUtils.equalsIgnoreCase(method, this.httpMethod);
    }

    public boolean isHttpMethodGet() {
        return this.isHttpMethod(Route.HTTP_METHOD_GET);
    }

    public boolean isHttpMethodPost() {
        return this.isHttpMethod(Route.HTTP_METHOD_POST);
    }

    public boolean isHttpMethodPut() {
        return this.isHttpMethod(Route.HTTP_METHOD_PUT);
    }

    public boolean isHttpMethodDelete() {
        return this.isHttpMethod(Route.HTTP_METHOD_DELETE);
    }

    public boolean isHttpMethodHead() {
        return this.isHttpMethod(Route.HTTP_METHOD_HEAD);
    }

    public boolean isHttpMethodOptions() {
        return this.isHttpMethod(Route.HTTP_METHOD_OPTIONS);
    }

    public boolean isHttpMethodWebSocket() {
        return this.isHttpMethod(Route.HTTP_METHOD_WEBSOCKET);
    }

    public String getUri() {
        return uri;
    }

    public Class<?> getControllerClass() {
        return controllerMethod != null ? controllerMethod.getDeclaringClass() : null;
    }

    public Method getControllerMethod() {
        return controllerMethod;
    }

    public FilterChain getFilterChain() {
        return filterChain;
    }

    public Map<String,RouteParameter> getParameters() {
        return parameters;
    }

    /**
     * Matches /index to /index or /me/1 to /person/{id}
     *
     * @return True if the actual route matches a raw rout. False if not.
     *
     */
    public boolean matches(String httpMethod, String uri) {
        if (this.httpMethod.equalsIgnoreCase(httpMethod)) {
            Matcher matcher = regex.matcher(uri);
            return matcher.matches();
        } else {
            return false;
        }
    }

    /**
     * This method does not do any decoding / encoding.
     *
     * If you want to decode you have to do it yourself.
     *
     * Most likely with:
     * http://docs.oracle.com/javase/6/docs/api/java/net/URI.html
     *
     * @param uri The whole encoded uri.
     * @return A map with all parameters of that uri. Encoded in => encoded out.
     */
    public Map<String, String> getPathParametersEncoded(String uri) {
        Map<String, String> map = Maps.newHashMap();

        Matcher m = regex.matcher(uri);

        if (m.matches()) {
            Iterator<String> it = this.parameters.keySet().iterator();
            for (int i = 1; i < m.groupCount() + 1; i++) {
                String parameterName = it.next();
                map.put(parameterName, m.group(i));
            }
        }

        return map;
    }

    /**
     * Gets a raw uri like "/{name}/id/*" and returns "/([^/]*)/id/*."
     *
     * Also handles regular expressions if defined inside routes:
     * For instance "/users/{username: [a-zA-Z][a-zA-Z_0-9]}" becomes
     * "/users/([a-zA-Z][a-zA-Z_0-9])"
     *
     * @return The converted regex with default matching regex - or the regex
     *          specified by the user.
     */
    protected static String convertRawUriToRegex(String rawUri) {

        // convert capturing groups in route regex to non-capturing groups
        // this is to avoid count mismatch of path params and groups in uri regex
        Matcher groupMatcher = Pattern.compile("\\(([^?].*)\\)").matcher(rawUri);
        String converted = groupMatcher.replaceAll("\\(?:$1\\)");

        Matcher matcher = PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE.matcher(converted);

        StringBuffer stringBuffer = new StringBuffer();

        while (matcher.find()) {

            // By convention group 3 is the regex if provided by the user.
            // If it is not provided by the user the group 3 is null.
            String namedVariablePartOfRoute = matcher.group(3);
            String namedVariablePartOfORouteReplacedWithRegex;

            if (namedVariablePartOfRoute != null) {
                // we convert that into a regex matcher group itself
                namedVariablePartOfORouteReplacedWithRegex
                    = "(" + Matcher.quoteReplacement(namedVariablePartOfRoute) + ")";
            } else {
                // we convert that into the default namedVariablePartOfRoute regex group
                namedVariablePartOfORouteReplacedWithRegex
                    = VARIABLE_ROUTES_DEFAULT_REGEX;
            }
            // we replace the current namedVariablePartOfRoute group
            matcher.appendReplacement(stringBuffer, namedVariablePartOfORouteReplacedWithRegex);

        }

        // .. and we append the tail to complete the stringBuffer
        matcher.appendTail(stringBuffer);

        return stringBuffer.toString();
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class NinjaModeHelper {

    static Logger logger = LoggerFactory.getLogger(NinjaModeHelper.class);

    /**
     * returns an empty Optional<NinjaMode> if no mode is set. Or the valid mode
     * set via a System Property called "ninja.mode".
     *
     * E.g. under mvn you can use mvn ... -Dninja.mode=prod or so. Valid values
     * for ninja.mode are "prod", "dev", "test".
     *
     * @return The valid mode set via a System Property called "ninja.mode"
     *          or Optional absent if we cannot get one.
     */
    public static Optional<NinjaMode> determineModeFromSystemProperties() {

        NinjaMode ninjaMode = null;

        // Get mode possibly set via a system property
        String modeFromGetSystemProperty = System
                .getProperty(NinjaConstant.MODE_KEY_NAME);

        // If the user specified a mode we set the mode accordingly:
        if (modeFromGetSystemProperty != null) {

            if (modeFromGetSystemProperty.equals(NinjaConstant.MODE_TEST)) {

                ninjaMode = NinjaMode.test;

            } else if (modeFromGetSystemProperty.equals(NinjaConstant.MODE_DEV)) {

                ninjaMode = NinjaMode.dev;

            } else if (modeFromGetSystemProperty.equals(NinjaConstant.MODE_PROD)) {

                ninjaMode = NinjaMode.prod;

            }

        }

        return Optional.ofNullable(ninjaMode);

    }

    /**
     * returns NinjaMode.dev if no mode is set. Or the valid mode
     * set via a System Property called "ninja.mode".
     *
     * E.g. under mvn you can use mvn ... -Dninja.mode=prod or so. Valid values
     * for ninja.mode are "prod", "dev", "test".
     *
     * @return The valid mode set via a System Property called "ninja.mode"
     *          or NinjaMode.dev if it is not set.
     */
    public static NinjaMode determineModeFromSystemPropertiesOrProdIfNotSet() {

        Optional<NinjaMode> ninjaModeOptional = determineModeFromSystemProperties();
        NinjaMode ninjaMode;

        if (!ninjaModeOptional.isPresent()) {
            ninjaMode = NinjaMode.prod;
        } else {
            ninjaMode = ninjaModeOptional.get();
        }

        logger.info("Ninja is running in mode {}", ninjaMode.toString());

        return ninjaMode;

    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

import java.io.InputStream;
import java.util.Enumeration;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ninja.Context;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;
import com.google.inject.Singleton;

/**
 * MimeTypes utils Adapted from play 1.2.4
 */
@Singleton
public class MimeTypes {
    private final Logger logger = LoggerFactory.getLogger(MimeTypes.class);

    private final String PROPERTY_MIMETYPE_PREFIX = "mimetype.";
    private final String DEFAULT_MIMET_TYPE_LOCATIONS = "ninja/utils/mime-types.properties";

    private final Properties mimetypes;
    private final Pattern extPattern;

    private final NinjaProperties ninjaProperties;

    @Inject
    public MimeTypes(NinjaProperties ninjaProperties) {
        this.ninjaProperties = ninjaProperties;
        this.extPattern = Pattern.compile("^.*\\.([^.]+)$");

        mimetypes = new Properties();
        initMimetypes();
    }

    /**
     * return the mimetype from a file name
     *
     * @param filename
     *            the file name
     * @return the mimetype or the empty string if not found
     */
    public String getMimeType(String filename) {
        return getMimeType(filename, "");
    }

    /**
     * return the mimetype from a file name.

     *
     * @param filename
     *            the file name
     * @param defaultMimeType
     *            the default mime type to return when no matching mimetype is
     *            found
     * @return the mimetype
     */
    public String getMimeType(String filename, String defaultMimeType) {
        Matcher matcher = extPattern.matcher(filename.toLowerCase());
        String ext = "";
        if (matcher.matches()) {
            ext = matcher.group(1);
        }
        if (ext.length() > 0) {
            String mimeType = mimetypes.getProperty(ext);
            if (mimeType == null) {
                return defaultMimeType;
            }
            return mimeType;
        }
        return defaultMimeType;
    }

    /**
     * return the content-type from a file name. If none is found returning
     * application/octet-stream

     * For a text-based content-type, also return the encoding suffix eg.
     * "text/plain; charset=utf-8"
     *
     * @param filename
     *            the file name
     * @return the content-type deduced from the file extension.
     */
    public String getContentType(Context context, String filename) {
        return getContentType(context, filename, "application/octet-stream");
    }

    /**
     * return the content-type from a file name.

     * For a text-based content-type, also return the encoding suffix eg.
     * "text/plain; charset=utf-8"
     *
     * @param filename
     *            the file name
     * @param defaultContentType
     *            the default content-type to return when no matching
     *            content-type is found
     * @return the content-type deduced from the file extension.
     */
    public String getContentType(Context context,
                                 String filename,
                                 String defaultContentType) {
        String contentType = getMimeType(filename, null);
        if (contentType == null) {
            contentType = defaultContentType;
        }
        if (contentType != null && contentType.startsWith("text/")) {
            // UTF-8 is fixed for now as ninja only supports utf-8 in files...
            return contentType + "; charset=utf-8";
        }
        return contentType;
    }

    /**
     * check the mimetype is referenced in the mimetypes database
     *
     * @param mimeType
     *            the mimeType to verify
     */
    public boolean isValidMimeType(String mimeType) {
        if (mimeType == null) {
            return false;
        } else if (mimeType.indexOf(";") != -1) {
            return mimetypes.contains(mimeType.split(";")[0]);
        } else {
            return mimetypes.contains(mimeType);
        }
    }

    private void initMimetypes() {

        // Load default mimetypes from the framework
        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(DEFAULT_MIMET_TYPE_LOCATIONS);) {
            mimetypes.load(is);
        } catch (Exception e) {
            logger.error("Failed to load mimetypes", e);
        }

        // Load custom mimetypes from the application configuration
        Enumeration confenum = ninjaProperties.getAllCurrentNinjaProperties()
                .propertyNames();

        while (confenum.hasMoreElements()) {
            String key = (String) confenum.nextElement();
            if (key.startsWith(PROPERTY_MIMETYPE_PREFIX)) {
                String type = key.substring(key.indexOf('.') + 1).toLowerCase();
                String value = (String) ninjaProperties.get(key);
                mimetypes.setProperty(type, value);
            }
        }
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.websockets;

import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.lang.StringUtils;

/**
 * Utilities for implementing WebSockets.
 *
 * @author jjlauer
 */
public class WebSocketUtils {

    // Attribute key for context storage of the endpoint instance to use
    // for continuation of a websocket handshake
    static public final String ATTRIBUTE_ENDPOINT = "ninja.websocket.endpoint";
    static public final String ATTRIBUTE_HANDSHAKE = "ninja.websocket.handshake";

    /**
     * Parses header `Sec-WebSocket-Protocol: chat, superchat` into a list
     * such as `chat`, and `superchat`.
     * @param value The header value
     * @return A set of values or null if value was null or empty
     */
    static public Set<String> parseProtocolRequestHeader(String value) {
        Set<String> subprotocols = new LinkedHashSet<>();

        if (!StringUtils.isEmpty(value)) {
            String[] values = value.split(",");
            for (String v : values) {
                subprotocols.add(v.trim());
            }
        }

        return subprotocols;
    }

}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja;

import com.google.common.collect.Lists;
import com.google.inject.Inject;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Set;
import ninja.params.ControllerMethodInvoker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
import java.util.List;
import java.util.Optional;
import ninja.ControllerMethods.ControllerMethod;
import ninja.utils.LambdaRoute;
import ninja.utils.MethodReference;
import ninja.utils.NinjaBaseDirectoryResolver;
import ninja.utils.NinjaProperties;
import ninja.utils.SwissKnife;
import ninja.application.ApplicationFilters;

public class RouteBuilderImpl implements RouteBuilder {
    private static final Logger log = LoggerFactory.getLogger(RouteBuilder.class);

    protected final static String GLOBAL_FILTERS_DEFAULT_LOCATION = "conf.Filters";

    private String httpMethod;
    private String uri;
    private Method functionalMethod;                // lamda routes can have this + maybe impl and targets
    private Optional<Method> implementationMethod;  // method to use for parameter/annotation extraction
    private Optional<Object> targetObject;          // instance to invoke
    private final NinjaProperties ninjaProperties;
    private Optional<List<Class<? extends Filter>>> globalFiltersOptional;
    private final List<Class<? extends Filter>> localFilters;
    private final NinjaBaseDirectoryResolver ninjaBaseDirectoryResolver;

    @Inject
    public RouteBuilderImpl(
            NinjaProperties ninjaProperties,
            NinjaBaseDirectoryResolver ninjaBaseDirectoryResolver) {
        this.implementationMethod = Optional.empty();
        this.targetObject = Optional.empty();
        this.ninjaProperties = ninjaProperties;
        this.ninjaBaseDirectoryResolver = ninjaBaseDirectoryResolver;
        this.globalFiltersOptional = Optional.empty();
        this.localFilters = Lists.newArrayList();
    }

    public RouteBuilderImpl GET() {
        httpMethod = Route.HTTP_METHOD_GET;
        return this;
    }

    public RouteBuilderImpl POST() {
        httpMethod = Route.HTTP_METHOD_POST;
        return this;
    }

    public RouteBuilderImpl PUT() {
        httpMethod = Route.HTTP_METHOD_PUT;
        return this;
    }

    public RouteBuilderImpl DELETE() {
        httpMethod = Route.HTTP_METHOD_DELETE;
        return this;
    }

    public RouteBuilderImpl OPTIONS() {
        httpMethod = Route.HTTP_METHOD_OPTIONS;
        return this;
    }

    public RouteBuilderImpl HEAD() {
        httpMethod = Route.HTTP_METHOD_HEAD;
        return this;
    }

    public RouteBuilderImpl WS() {
        httpMethod = Route.HTTP_METHOD_WEBSOCKET;
        return this;
    }

    public RouteBuilderImpl METHOD(String method) {
        httpMethod = method;
        return this;
    }

    @Override
    public void with(Class<?> controllerClass, String controllerMethod) {
        this.functionalMethod
            = verifyControllerMethod(controllerClass, controllerMethod);
    }

    @Override @Deprecated
    public void with(MethodReference methodRef) {
        with(methodRef.getDeclaringClass(), methodRef.getMethodName());
    }

    @Override @Deprecated
    public void with(final Result result) {
        with(ControllerMethods.of(() -> result));
    }

    @Override
    public Void with(ControllerMethod controllerMethod) {
        LambdaRoute lambdaRoute = LambdaRoute.resolve(controllerMethod);
        this.functionalMethod = lambdaRoute.getFunctionalMethod();
        this.implementationMethod = lambdaRoute.getImplementationMethod();
        this.targetObject = lambdaRoute.getTargetObject();
        return null;
    }

    @Override
    public RouteBuilder globalFilters(List<Class<? extends Filter>> filtersToAdd) {
        this.globalFiltersOptional = Optional.of(filtersToAdd);
        return this;
    }

    @Override
    public RouteBuilder globalFilters(Class<? extends Filter> ... filtersToAdd) {
        List<Class<? extends Filter>> globalFiltersTemp = Lists.newArrayList(filtersToAdd);
        globalFilters(globalFiltersTemp);
        return this;
    }

    @Override
    public RouteBuilder filters(List<Class<? extends Filter>> filtersToAdd) {
        this.localFilters.addAll(filtersToAdd);
        return this;
    }

    @Override
    public RouteBuilder filters(Class<? extends Filter> ... filtersToAdd) {
        List<Class<? extends Filter>> filtersTemp = Lists.newArrayList(filtersToAdd);
        filters(filtersTemp);
        return this;
    }

    @Override
    public RouteBuilder route(String uri) {
        this.uri = uri;
        return this;
    }

    /**
     * Routes are usually defined in conf/Routes.java as
     * router.GET().route("/teapot").with(FilterController.class, "teapot");
     *
     * Unfortunately "teapot" is not checked by the compiler. We do that here at
     * runtime.
     *
     * We are reloading when there are changes. So this is almost as good as
     * compile time checking.
     *
     * @param controller
     *            The controller class
     * @param controllerMethod
     *            The method
     * @return The actual method
     */
    private Method verifyControllerMethod(Class<?> controllerClass,
                                          String controllerMethod) {
        try {
            Method methodFromQueryingClass = null;

            // 1. Make sure method is in class
            // 2. Make sure only one method is there. Otherwise we cannot really
            // know what to do with the parameters.
            for (Method method : controllerClass.getMethods()) {
                if (method.getName().equals(controllerMethod)) {
                    if (methodFromQueryingClass == null) {
                        methodFromQueryingClass = method;
                    } else {
                        throw new NoSuchMethodException();
                    }
                }
            }

            if (methodFromQueryingClass == null) {
                throw new NoSuchMethodException();
            }

            // make sure that the return type of that controller method
            // is of type Result.
            if (Result.class.isAssignableFrom(methodFromQueryingClass.getReturnType())) {
                return methodFromQueryingClass;
            } else {
                throw new NoSuchMethodException();
            }

        } catch (SecurityException e) {
            log.error(
                    "Error while checking for valid Controller / controllerMethod combination",
                    e);
        } catch (NoSuchMethodException e) {
            log.error("Error in route configuration!!!");
            log.error("Can not find Controller " + controllerClass.getName()
                    + " and method " + controllerMethod);
            log.error("Hint: make sure the controller returns a ninja.Result!");
            log.error("Hint: Ninja does not allow more than one method with the same name!");
        }
        return null;
    }

    /**
     * Build the route.
     * @param injector The injector to build the route with
     * @return The built route
     */
    public Route buildRoute(Injector injector) {
        if (functionalMethod == null) {
            log.error("Error in route configuration for {}", uri);
            throw new IllegalStateException("Route missing a controller method");
        }

        // Calculate filters
        LinkedList<Class<? extends Filter>> allFilters = new LinkedList<>();

        allFilters.addAll(calculateGlobalFilters(this.globalFiltersOptional, injector));

        allFilters.addAll(this.localFilters);

        allFilters.addAll(calculateFiltersForClass(functionalMethod.getDeclaringClass()));
        FilterWith filterWith = functionalMethod.getAnnotation(FilterWith.class);
        if (filterWith != null) {
            allFilters.addAll(Arrays.asList(filterWith.value()));
        }

        FilterChain filterChain = buildFilterChain(injector, allFilters);

        return new Route(httpMethod, uri, functionalMethod, filterChain);
    }

    private List<Class<? extends Filter>> calculateGlobalFilters(
            Optional<List<Class<? extends Filter>>> globalFiltersList,
            Injector injector) {
        List<Class<? extends Filter>> allFilters = Lists.newArrayList();
        // Setting globalFilters in route will deactivate the filters defined
        // by conf.Filters
        if (globalFiltersList.isPresent()) {
            allFilters.addAll(globalFiltersList.get());
        } else {
            String globalFiltersWithPrefixMaybe
                    = ninjaBaseDirectoryResolver.resolveApplicationClassName(GLOBAL_FILTERS_DEFAULT_LOCATION);

            if (SwissKnife.doesClassExist(globalFiltersWithPrefixMaybe, this)) {
                try {
                    Class<?> globalFiltersClass = Class.forName(globalFiltersWithPrefixMaybe);
                    ApplicationFilters globalFilters = (ApplicationFilters) injector.getInstance(globalFiltersClass);
                    globalFilters.addFilters(allFilters);
                } catch (Exception exception) {
                    // That simply means the user did not configure conf.Filters.
                }
            }
        }

        return allFilters;

    }

    private FilterChain buildFilterChain(Injector injector,
                                         LinkedList<Class<? extends Filter>> filters) {

        if (filters.isEmpty()) {

            // either target object (functional method) or guice will create new instance
            Provider<?> targetProvider = (targetObject.isPresent() ?
                Providers.of(targetObject.get())
                    : injector.getProvider(functionalMethod.getDeclaringClass()));

            // invoke functional method with optionally using impl for argument extraction
            ControllerMethodInvoker methodInvoker
                = ControllerMethodInvoker.build(
                    functionalMethod, implementationMethod.orElse(functionalMethod), injector, ninjaProperties);

            return new FilterChainEnd(targetProvider, methodInvoker);

        } else {

            Class<? extends Filter> filter = filters.pop();

            Provider<? extends Filter> filterProvider = injector.getProvider(filter);

            return new FilterChainImpl(filterProvider,buildFilterChain(injector, filters));

        }
    }

    private Set<Class<? extends Filter>> calculateFiltersForClass(
            Class controller) {
        LinkedHashSet<Class<? extends Filter>> filters = new LinkedHashSet<>();

        //
        // Step up the superclass tree, so that superclass filters come first
        //

        // Superclass
        if (controller.getSuperclass() != null) {
            filters.addAll(calculateFiltersForClass(controller.getSuperclass()));
        }

        // Interfaces
        if (controller.getInterfaces() != null) {
            for (Class clazz : controller.getInterfaces()) {
                filters.addAll(calculateFiltersForClass(clazz));
            }
        }

        // Now add from here
        FilterWith filterWith = (FilterWith) controller
                .getAnnotation(FilterWith.class);
        if (filterWith != null) {
            filters.addAll(Arrays.asList(filterWith.value()));
        }

        // And return
        return filters;
    }
}


/**
 * Copyright (C) 2012-2019 the original author or authors.
 *
 * 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 ninja.utils;

import java.lang.invoke.SerializedLambda;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Lambdas {
    private static final Logger log = LoggerFactory.getLogger(Lambdas.class);

    /**
     * The kind of lambda.  Lambdas in Java can either be references to methods
     * or anonymously defined.
     * https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
     */
    public static enum Kind {
        // Reference to a static method (e.g. ContainingClass::staticMethodName)
        STATIC_METHOD_REFERENCE,
        // Reference to an instance method of a specific object (e.g. specificObject::instanceMethodName)
        SPECIFIC_INSTANCE_METHOD_REFERENCE,
        // Reference to an instance method of an arbitrary object of a particular type (e.g. ContainingClass::instanceMethodName)
        ANY_INSTANCE_METHOD_REFERENCE,
        // Anonymously defined (e.g. () -> return "a"; )
        ANONYMOUS_METHOD_REFERENCE
    }

    public static class LambdaInfo {
        private final Object lambda;
        private final Kind kind;
        private final SerializedLambda serializedLambda;
        private final Method functionalMethod;
        private final Method implementationMethod;

        public LambdaInfo(Object lambda, Kind kind, SerializedLambda serializedLambda, Method functionalMethod, Method implementationMethod) {
            this.lambda = lambda;
            this.kind = kind;
            this.serializedLambda = serializedLambda;
            this.functionalMethod = functionalMethod;
            this.implementationMethod = implementationMethod;
        }

        public Object getLambda() {
            return lambda;
        }

        public Kind getKind() {
            return kind;
        }

        public SerializedLambda getSerializedLambda() {
            return serializedLambda;
        }

        public Method getFunctionalMethod() {
            return functionalMethod;
        }

        public Method getImplementationMethod() {
            return implementationMethod;
        }

        public boolean areMethodParameterCountsEqual() {
            // in case of captured arguments sometimes these do not match
            return this.functionalMethod.getParameterCount() ==
                    this.implementationMethod.getParameterCount();
        }

        @Override
        public String toString() {
            return new StringBuilder()
                .append("kind=").append(kind)
                .append(" func=").append(functionalMethod)
                .append(" impl=").append(implementationMethod)
                .append(" serialized=").append(serializedLambda)
                .toString();
        }
    }

    public static LambdaInfo reflect(Object lambda) {
        Objects.requireNonNull(lambda);

        // note: this throws a runtime exception if the lambda isn't serializable
        // or if the object isn't actually a lambda (e.g. an anon class)
        SerializedLambda serializedLambda = getSerializedLambda(lambda);

        Method functionalMethod;
        try {
            functionalMethod = getMethod(lambda.getClass(),
                serializedLambda.getFunctionalInterfaceMethodName());

            // important: only way classes other than the creator can invoke it
            functionalMethod.setAccessible(true);
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            throw new RuntimeException("Unable to getFunctionalMethod", e);
        }

        Method implementationMethod;
        try {
            implementationMethod = getImplementationMethod(serializedLambda);
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            throw new RuntimeException("Unable to getImplementationMethod", e);
        }

        Kind kind;

        // https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandleInfo.html
        int implMethodKind = serializedLambda.getImplMethodKind();

        if (implMethodKind == 6) {  // REF_invokeStatic
            if (serializedLambda.getImplMethodName().startsWith("lambda$")) {
                kind = Kind.ANONYMOUS_METHOD_REFERENCE;
            } else {
                kind = Kind.STATIC_METHOD_REFERENCE;
            }
        } else {
            if (serializedLambda.getCapturedArgCount() > 0) {
                kind = Kind.SPECIFIC_INSTANCE_METHOD_REFERENCE;
            } else {
                kind = Kind.ANY_INSTANCE_METHOD_REFERENCE;
            }
        }

        return new LambdaInfo(lambda, kind, serializedLambda, functionalMethod, implementationMethod);
    }

    /**
     * Tries to get a SerializedLambda from an Object by searching the class
     * hierarchy for a writeReplace method.  The lambda must
     * be serializable in order for this method to return a value.
     * @param lambda An object that is an instance of a functional interface.
     * @return The SerializedLambda
     */
    public static SerializedLambda getSerializedLambda(Object lambda) {
        Objects.requireNonNull(lambda);

        if (!(lambda instanceof java.io.Serializable)) {
            throw new IllegalArgumentException("Functional object does not implement java.io.Serializable");
        }

        for (Class<?> clazz = lambda.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Method replaceMethod = clazz.getDeclaredMethod("writeReplace");
                replaceMethod.setAccessible(true);
                Object serializedForm = replaceMethod.invoke(lambda);

                if (serializedForm instanceof SerializedLambda) {
                    return (SerializedLambda) serializedForm;
                }
            } catch (NoSuchMethodError e) {
                // fall through the loop and try the next class
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException("Functional object is not a lambda");
            } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                throw new RuntimeException("Unable to cleanly serialize lambda", e);
            }
        }

        throw new RuntimeException("writeReplace method not found");
    }

    public static Method getFunctionalMethod(SerializedLambda serializedLambda) throws NoSuchMethodException, ClassNotFoundException {
        return getMethod(serializedLambda.getFunctionalInterfaceClass(),
            serializedLambda.getFunctionalInterfaceMethodName());
    }

    public static Method getImplementationMethod(SerializedLambda serializedLambda) throws NoSuchMethodException, ClassNotFoundException {
        return getMethod(serializedLambda.getImplClass(),
            serializedLambda.getImplMethodName());
    }

    public static Method getMethod(String className, String methodName) throws NoSuchMethodException, ClassNotFoundException {
        Class<?> clazz = Class.forName(className.replace('/', '.'));
        return getMethod(clazz, methodName);
    }

    public static Method getMethod(Class<?> clazz, String methodName) throws NoSuchMethodException, ClassNotFoundException {
        while (clazz != null) {
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getName().equals(methodName)) {
                    return method;
                }
            }
            // no match then try interfaces in order
            for (Class<?> interfaceClass : clazz.getInterfaces()) {
                for (Method method : interfaceClass.getDeclaredMethods()) {
                    return method;
                }
            }
            clazz = clazz.getSuperclass();
        }

        throw new NoSuchMethodException("Method " + methodName + " not found in " + clazz);
    }

}