001/*
002 * Copyright 2012-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.cli.command.run;
018
019import java.io.File;
020import java.io.IOException;
021import java.lang.reflect.Method;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027import java.util.concurrent.TimeUnit;
028import java.util.logging.Level;
029
030import org.springframework.boot.cli.app.SpringApplicationLauncher;
031import org.springframework.boot.cli.compiler.GroovyCompiler;
032import org.springframework.boot.cli.util.ResourceUtils;
033
034/**
035 * Compiles Groovy code running the resulting classes using a {@code SpringApplication}.
036 * Takes care of threading and class-loading issues and can optionally monitor sources for
037 * changes.
038 *
039 * @author Phillip Webb
040 * @author Dave Syer
041 */
042public class SpringApplicationRunner {
043
044        private static int watcherCounter = 0;
045
046        private static int runnerCounter = 0;
047
048        private final Object monitor = new Object();
049
050        private final SpringApplicationRunnerConfiguration configuration;
051
052        private final String[] sources;
053
054        private final String[] args;
055
056        private final GroovyCompiler compiler;
057
058        private RunThread runThread;
059
060        private FileWatchThread fileWatchThread;
061
062        /**
063         * Create a new {@link SpringApplicationRunner} instance.
064         * @param configuration the configuration
065         * @param sources the files to compile/watch
066         * @param args input arguments
067         */
068        SpringApplicationRunner(final SpringApplicationRunnerConfiguration configuration,
069                        String[] sources, String... args) {
070                this.configuration = configuration;
071                this.sources = sources.clone();
072                this.args = args.clone();
073                this.compiler = new GroovyCompiler(configuration);
074                int level = configuration.getLogLevel().intValue();
075                if (level <= Level.FINER.intValue()) {
076                        System.setProperty(
077                                        "org.springframework.boot.cli.compiler.grape.ProgressReporter",
078                                        "detail");
079                        System.setProperty("trace", "true");
080                }
081                else if (level <= Level.FINE.intValue()) {
082                        System.setProperty("debug", "true");
083                }
084                else if (level == Level.OFF.intValue()) {
085                        System.setProperty("spring.main.banner-mode", "OFF");
086                        System.setProperty("logging.level.ROOT", "OFF");
087                        System.setProperty(
088                                        "org.springframework.boot.cli.compiler.grape.ProgressReporter",
089                                        "none");
090                }
091        }
092
093        /**
094         * Compile and run the application.
095         * @throws Exception on error
096         */
097        public void compileAndRun() throws Exception {
098                synchronized (this.monitor) {
099                        try {
100                                stop();
101                                Object[] compiledSources = compile();
102                                monitorForChanges();
103                                // Run in new thread to ensure that the context classloader is setup
104                                this.runThread = new RunThread(compiledSources);
105                                this.runThread.start();
106                                this.runThread.join();
107                        }
108                        catch (Exception ex) {
109                                if (this.fileWatchThread == null) {
110                                        throw ex;
111                                }
112                                else {
113                                        ex.printStackTrace();
114                                }
115                        }
116                }
117        }
118
119        public void stop() {
120                synchronized (this.monitor) {
121                        if (this.runThread != null) {
122                                this.runThread.shutdown();
123                                this.runThread = null;
124                        }
125                }
126        }
127
128        private Object[] compile() throws IOException {
129                Object[] compiledSources = this.compiler.compile(this.sources);
130                if (compiledSources.length == 0) {
131                        throw new RuntimeException(
132                                        "No classes found in '" + Arrays.toString(this.sources) + "'");
133                }
134                return compiledSources;
135        }
136
137        private void monitorForChanges() {
138                if (this.fileWatchThread == null && this.configuration.isWatchForFileChanges()) {
139                        this.fileWatchThread = new FileWatchThread();
140                        this.fileWatchThread.start();
141                }
142        }
143
144        /**
145         * Thread used to launch the Spring Application with the correct context classloader.
146         */
147        private class RunThread extends Thread {
148
149                private final Object monitor = new Object();
150
151                private final Object[] compiledSources;
152
153                private Object applicationContext;
154
155                /**
156                 * Create a new {@link RunThread} instance.
157                 * @param compiledSources the sources to launch
158                 */
159                RunThread(Object... compiledSources) {
160                        super("runner-" + (runnerCounter++));
161                        this.compiledSources = compiledSources;
162                        if (compiledSources.length != 0 && compiledSources[0] instanceof Class) {
163                                setContextClassLoader(((Class<?>) compiledSources[0]).getClassLoader());
164                        }
165                        setDaemon(true);
166                }
167
168                @Override
169                public void run() {
170                        synchronized (this.monitor) {
171                                try {
172                                        this.applicationContext = new SpringApplicationLauncher(
173                                                        getContextClassLoader()).launch(this.compiledSources,
174                                                                        SpringApplicationRunner.this.args);
175                                }
176                                catch (Exception ex) {
177                                        ex.printStackTrace();
178                                }
179                        }
180                }
181
182                /**
183                 * Shutdown the thread, closing any previously opened application context.
184                 */
185                public void shutdown() {
186                        synchronized (this.monitor) {
187                                if (this.applicationContext != null) {
188                                        try {
189                                                Method method = this.applicationContext.getClass()
190                                                                .getMethod("close");
191                                                method.invoke(this.applicationContext);
192                                        }
193                                        catch (NoSuchMethodException ex) {
194                                                // Not an application context that we can close
195                                        }
196                                        catch (Exception ex) {
197                                                ex.printStackTrace();
198                                        }
199                                        finally {
200                                                this.applicationContext = null;
201                                        }
202                                }
203                        }
204                }
205
206        }
207
208        /**
209         * Thread to watch for file changes and trigger recompile/reload.
210         */
211        private class FileWatchThread extends Thread {
212
213                private long previous;
214
215                private List<File> sources;
216
217                FileWatchThread() {
218                        super("filewatcher-" + (watcherCounter++));
219                        this.previous = 0;
220                        this.sources = getSourceFiles();
221                        for (File file : this.sources) {
222                                if (file.exists()) {
223                                        long current = file.lastModified();
224                                        if (current > this.previous) {
225                                                this.previous = current;
226                                        }
227                                }
228                        }
229                        setDaemon(false);
230                }
231
232                private List<File> getSourceFiles() {
233                        List<File> sources = new ArrayList<File>();
234                        for (String source : SpringApplicationRunner.this.sources) {
235                                List<String> paths = ResourceUtils.getUrls(source,
236                                                SpringApplicationRunner.this.compiler.getLoader());
237                                for (String path : paths) {
238                                        try {
239                                                URL url = new URL(path);
240                                                if ("file".equals(url.getProtocol())) {
241                                                        sources.add(new File(url.getFile()));
242                                                }
243                                        }
244                                        catch (MalformedURLException ex) {
245                                                // Ignore
246                                        }
247                                }
248                        }
249                        return sources;
250                }
251
252                @Override
253                public void run() {
254                        while (true) {
255                                try {
256                                        Thread.sleep(TimeUnit.SECONDS.toMillis(1));
257                                        for (File file : this.sources) {
258                                                if (file.exists()) {
259                                                        long current = file.lastModified();
260                                                        if (this.previous < current) {
261                                                                this.previous = current;
262                                                                compileAndRun();
263                                                        }
264                                                }
265                                        }
266                                }
267                                catch (InterruptedException ex) {
268                                        Thread.currentThread().interrupt();
269                                }
270                                catch (Exception ex) {
271                                        // Swallow, will be reported by compileAndRun
272                                }
273                        }
274                }
275
276        }
277
278}