All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.grails.cli.app.GrailsApplicationRunner Maven / Gradle / Ivy

There is a newer version: 2023.1.0-RC1
Show newest version
/*
 * Copyright 2012-2023 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
 *
 *      https://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 org.grails.cli.app;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import org.grails.cli.compiler.GroovyCompiler;
import org.grails.cli.util.ResourceUtils;

/**
 * Compiles Groovy code running the resulting classes using a {@code SpringApplication}.
 * Takes care of threading and class-loading issues and can optionally monitor sources for
 * changes.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @author Michael Yan
 * @since 2022.1.0
 */
public class GrailsApplicationRunner {

    private static int watcherCounter = 0;

    private static int runnerCounter = 0;

    private final Object monitor = new Object();

    private final GrailsApplicationRunnerConfiguration configuration;

    private final String[] sources;

    private final String[] args;

    private final GroovyCompiler compiler;

    private RunThread runThread;

    private FileWatchThread fileWatchThread;

    /**
     * Create a new {@link GrailsApplicationRunner} instance.
     *
     * @param configuration the configuration
     * @param sources       the files to compile/watch
     * @param args          input arguments
     */
    public GrailsApplicationRunner(GrailsApplicationRunnerConfiguration configuration, String[] sources, String... args) {
        this.configuration = configuration;
        this.sources = sources.clone();
        this.args = args.clone();
        this.compiler = new GroovyCompiler(configuration);
        int level = configuration.getLogLevel().intValue();
        if (level <= Level.FINER.intValue()) {
            System.setProperty("org.grails.cli.compiler.grape.ProgressReporter", "detail");
            System.setProperty("trace", "true");
        }
        else if (level <= Level.FINE.intValue()) {
            System.setProperty("debug", "true");
        }
        else if (level == Level.OFF.intValue()) {
            System.setProperty("spring.main.banner-mode", "OFF");
            System.setProperty("logging.level.ROOT", "OFF");
            System.setProperty("org.grails.cli.compiler.grape.ProgressReporter", "none");
        }
    }

    /**
     * Compile and run the application.
     *
     * @throws Exception on error
     */
    public void compileAndRun() throws Exception {
        synchronized (this.monitor) {
            try {
                stop();
                Class[] compiledSources = compile();
                monitorForChanges();
                // Run in new thread to ensure that the context classloader is set up
                this.runThread = new RunThread(compiledSources);
                this.runThread.start();
                this.runThread.join();
            }
            catch (Exception ex) {
                if (this.fileWatchThread == null) {
                    throw ex;
                }
                else {
                    ex.printStackTrace();
                }
            }
        }
    }

    public void stop() {
        synchronized (this.monitor) {
            if (this.runThread != null) {
                this.runThread.shutdown();
                this.runThread = null;
            }
        }
    }

    private Class[] compile() throws IOException {
        Class[] compiledSources = this.compiler.compile(this.sources);
        if (compiledSources.length == 0) {
            throw new RuntimeException("No classes found in '" + Arrays.toString(this.sources) + "'");
        }
        return compiledSources;
    }

    private void monitorForChanges() {
        if (this.fileWatchThread == null && this.configuration.isWatchForFileChanges()) {
            this.fileWatchThread = new FileWatchThread();
            this.fileWatchThread.start();
        }
    }

    /**
     * Thread used to launch the Spring Application with the correct context classloader.
     */
    private class RunThread extends Thread {

        private final Object monitor = new Object();

        private final Class[] compiledSources;

        private Object applicationContext;

        /**
         * Create a new {@link RunThread} instance.
         *
         * @param compiledSources the sources to launch
         */
        RunThread(Class... compiledSources) {
            super("runner-" + (runnerCounter++));
            this.compiledSources = compiledSources;
            if (compiledSources.length != 0) {
                setContextClassLoader(compiledSources[0].getClassLoader());
            }
            setDaemon(true);
        }

        @Override
        public void run() {
            synchronized (this.monitor) {
                try {
                    this.applicationContext = new GrailsApplicationLauncher(getContextClassLoader())
                            .launch(this.compiledSources, GrailsApplicationRunner.this.args);
                }
                catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }

        /**
         * Shutdown the thread, closing any previously opened application context.
         */
        void shutdown() {
            synchronized (this.monitor) {
                if (this.applicationContext != null) {
                    try {
                        Method method = this.applicationContext.getClass().getMethod("close");
                        method.invoke(this.applicationContext);
                    }
                    catch (NoSuchMethodException ex) {
                        // Not an application context that we can close
                    }
                    catch (Exception ex) {
                        ex.printStackTrace();
                    }
                    finally {
                        this.applicationContext = null;
                    }
                }
            }
        }

    }

    /**
     * Thread to watch for file changes and trigger recompile/reload.
     */
    private class FileWatchThread extends Thread {

        private long previous;

        private List sources;

        FileWatchThread() {
            super("filewatcher-" + (watcherCounter++));
            this.previous = 0;
            this.sources = getSourceFiles();
            for (File file : this.sources) {
                if (file.exists()) {
                    long current = file.lastModified();
                    if (current > this.previous) {
                        this.previous = current;
                    }
                }
            }
            setDaemon(false);
        }

        private List getSourceFiles() {
            List sources = new ArrayList<>();
            for (String source : GrailsApplicationRunner.this.sources) {
                List paths = ResourceUtils.getUrls(source, GrailsApplicationRunner.this.compiler.getLoader());
                for (String path : paths) {
                    try {
                        URL url = new URL(path);
                        if ("file".equals(url.getProtocol())) {
                            sources.add(new File(url.getFile()));
                        }
                    }
                    catch (MalformedURLException ex) {
                        // Ignore
                    }
                }
            }
            return sources;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(TimeUnit.SECONDS.toMillis(1));
                    for (File file : this.sources) {
                        if (file.exists()) {
                            long current = file.lastModified();
                            if (this.previous < current) {
                                this.previous = current;
                                compileAndRun();
                            }
                        }
                    }
                }
                catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
                catch (Exception ex) {
                    // Swallow, will be reported by compileAndRun
                }
            }
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy