/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby; import java.io.File; 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.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.maven.Maven; import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.DefaultMavenExecutionRequest; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Execute; 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; import org.apache.maven.project.MavenProject; import org.jooby.run.Watcher; import javaslang.control.Try; @Mojo(name = "run", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST) @Execute(phase = LifecyclePhase.TEST_COMPILE) public class JoobyMojo extends AbstractMojo { private static class ShutdownHook extends Thread { private Log log; private List<Command> commands; private Watcher watcher; public ShutdownHook(final Log log, final List<Command> commands) { this.log = log; this.commands = commands; setDaemon(true); } @Override public void run() { if (watcher != null) { log.info("stopping: watcher"); Try.run(watcher::stop).onFailure(ex -> log.debug("Stop of watcher resulted in error", ex)); } commands.forEach(cmd -> { log.info("stopping: " + cmd); Try.run(cmd::stop).onFailure(ex -> log.error("Stop of " + cmd + " resulted in error", ex)); }); } } @Parameter(defaultValue = "${project}", required = true, readonly = true) private MavenProject mavenProject; @Parameter(defaultValue = "${session}", required = true, readonly = true) protected MavenSession session; @Parameter(property = "main.class", defaultValue = "${application.class}") protected String mainClass; @Parameter(defaultValue = "${project.build.outputDirectory}") private String buildOutputDirectory; @Parameter(property = "jooby.commands") private List<ExternalCommand> commands; @Parameter(property = "jooby.vmArgs") private List<String> vmArgs; @Parameter(property = "jooby.includes") private List<String> includes; @Parameter(property = "jooby.watchDirs") private List<String> watchDirs; @Parameter(property = "jooby.excludes") private List<String> excludes; @Parameter(property = "application.debug", defaultValue = "true") private String debug; @Parameter(defaultValue = "${plugin.artifacts}") private List<org.apache.maven.artifact.Artifact> pluginArtifacts; @Parameter(property = "compiler", defaultValue = "on") private String compiler; @Component protected Maven maven; @Parameter(property = "application.fork", defaultValue = "false") private boolean fork = false; @SuppressWarnings("unchecked") @Override public void execute() throws MojoExecutionException, MojoFailureException { Set<File> appcp = new LinkedHashSet<>(); // public / config, etc.. appcp.addAll(resources(mavenProject.getResources())); // target/classes appcp.add(new File(buildOutputDirectory)); // references project Set<Artifact> references = references(mavenProject); Set<File> refbasedir = refbasedir(mavenProject, references); Set<File> refcp = refcp(refbasedir); appcp.addAll(refcp); // *.jar Set<Artifact> artifacts = new LinkedHashSet<Artifact>(mavenProject.getArtifacts()); artifacts.forEach(artifact -> { if (!"pom".equals(artifact.getType())) { // ignore self reference appcp.add(new File(artifact.getFile().getAbsolutePath())); } }); Set<File> classpath = new LinkedHashSet<>(); File hotreload = extra(pluginArtifacts, "jooby-run").get(); File jbossModules = extra(pluginArtifacts, "jboss-modules").get(); classpath.add(hotreload); classpath.add(jbossModules); // prepare commands List<Command> cmds = new ArrayList<>(); if (commands != null && commands.size() > 0) { cmds.addAll(this.commands); } // watch dir List<File> watchDirs = new ArrayList<>(); watchDirs.add(mavenProject.getBasedir()); watchDirs.addAll(refbasedir); if (this.watchDirs != null) { this.watchDirs.forEach(f -> watchDirs.add(new File(f))); } // includes/excludes pattern String includes = null; if (this.includes != null && this.includes.size() > 0) { includes = this.includes.stream().collect(Collectors.joining(File.pathSeparator)); } String excludes = null; if (this.excludes != null && this.excludes.size() > 0) { excludes = this.excludes.stream().collect(Collectors.joining(File.pathSeparator)); } String watchDirStr = watchDirs.stream().filter(File::exists) .map(File::getAbsolutePath) .collect(Collectors.joining(File.pathSeparator)); // moduleId String mId = mavenProject.getGroupId() + "." + mavenProject.getArtifactId(); // logback and application.version setLogback(); System.setProperty("application.version", mavenProject.getVersion()); // fork? Command runapp = fork ? new RunForkedApp(mavenProject.getBasedir(), debug, vmArgs, classpath, mId, mainClass, appcp, includes, excludes, watchDirStr) : new RunApp(mId, mainClass, appcp, includes, excludes, watchDirs); // run app at the end cmds.add(runapp); for (Command cmd : cmds) { cmd.setWorkdir(mavenProject.getBasedir()); getLog().debug("cmd: " + cmd.debug()); } Watcher watcher = setupCompiler(mavenProject, compiler, goal -> { maven.execute(DefaultMavenExecutionRequest.copy(session.getRequest()) .setGoals(Arrays.asList(goal))); }); ShutdownHook shutdownHook = new ShutdownHook(getLog(), cmds); shutdownHook.watcher = watcher; /** * Shutdown hook */ Runtime.getRuntime().addShutdownHook(shutdownHook); if (watcher != null) { watcher.start(); } /** * Start process */ for (Command cmd : cmds) { try { getLog().debug("Starting process: " + cmd.debug()); cmd.execute(); } catch (Exception ex) { throw new MojoFailureException("Execution of " + cmd + " resulted in error", ex); } } } @SuppressWarnings("unchecked") private Set<Artifact> references(final MavenProject project) { MavenProject parent = project.getParent(); if (parent != null) { List<String> modules = parent.getModules(); if (modules != null) { Set<Artifact> artifacts = new LinkedHashSet<Artifact>(mavenProject.getArtifacts()); String groupId = project.getGroupId(); String version = project.getVersion(); return artifacts.stream() .filter(a -> a.getGroupId().equals(groupId) && a.getVersion().equals(version) && modules.contains(a.getArtifactId())) .collect(Collectors.toSet()); } } return Collections.emptySet(); } private Set<File> refbasedir(final MavenProject project, final Set<Artifact> references) { Set<File> cp = new LinkedHashSet<>(); for (Artifact reference : references) { File basedir = project.getParent().getBasedir(); cp.add(new File(basedir, reference.getArtifactId())); } return cp; } private Set<File> refcp(final Set<File> files) { Set<File> cp = new LinkedHashSet<>(); for (File basedir : files) { cp.add(new File(new File(basedir, "target"), "classes")); } return cp; } @SuppressWarnings("unchecked") private static Watcher setupCompiler(final MavenProject project, final String compiler, final Consumer<String> task) throws MojoFailureException { File eclipseClasspath = new File(project.getBasedir(), ".classpath"); if ("off".equalsIgnoreCase(compiler) || eclipseClasspath.exists()) { return null; } List<File> resources = resources(project.getResources()); resources.add(0, new File(project.getBuild().getSourceDirectory())); List<Path> paths = resources.stream() .filter(File::exists) .map(File::toPath) .collect(Collectors.toList()); try { ClassLoader backloader = Thread.currentThread().getContextClassLoader(); return new Watcher((kind, path) -> { ClassLoader currentloader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(backloader); if (path.toString().endsWith(".java")) { task.accept("compile"); } else if (path.toString().endsWith(".conf") || path.toString().endsWith(".properties")) { task.accept("compile"); } } finally { Thread.currentThread().setContextClassLoader(currentloader); } }, paths.toArray(new Path[paths.size()])); } catch (Exception ex) { throw new MojoFailureException("Can't compile source code", ex); } } private void setLogback() { // logback File[] logbackFiles = {localFile("conf", "logback-test.xml"), localFile("conf", "logback.dev.xml"), localFile("conf", "logback.xml") }; for (File logback : logbackFiles) { if (logback.exists()) { System.setProperty("logback.configurationFile", logback.getAbsolutePath()); break; } } } private File localFile(final String... paths) { File result = mavenProject.getBasedir(); for (String path : paths) { result = new File(result, path); } return result; } private static List<File> resources(final Iterable<Resource> resources) { List<File> result = new ArrayList<>(); for (Resource resource : resources) { String dir = resource.getDirectory(); File file = new File(dir); if (file.exists()) { result.add(file); } } return result; } private Optional<File> extra(final List<Artifact> artifacts, final String name) { for (Artifact artifact : artifacts) { for (String tail : artifact.getDependencyTrail()) { if (tail.contains(name)) { return Optional.of(artifact.getFile()); } } } return Optional.empty(); } }