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