
fish.payara.maven.plugins.micro.AutoDeployHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of payara-micro-maven-plugin Show documentation
Show all versions of payara-micro-maven-plugin Show documentation
Payara Micro Maven Plugin that incorporates payara-micro with the produced artifact
/*
*
* Copyright (c) 2023-24 Payara Foundation and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://github.com/payara/Payara/blob/master/LICENSE.txt
* See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* The Payara Foundation designates this particular file as subject to the "Classpath"
* exception as provided by the Payara Foundation in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package fish.payara.maven.plugins.micro;
import static fish.payara.maven.plugins.micro.Configuration.INOTIFY_USER_LIMIT_REACHED_MESSAGE;
import static fish.payara.maven.plugins.micro.Configuration.WATCH_SERVICE_ERROR_MESSAGE;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.Invoker;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.openqa.selenium.JavascriptExecutor;
/**
*
* @author Gaurav Gupta
*/
public class AutoDeployHandler implements Runnable {
private final StartMojo start;
private final MavenProject project;
private final File webappDirectory;
private final Log log;
private final ExecutorService executorService;
private WatchService watchService;
private Future> buildReloadTask;
private final AtomicBoolean cleanPending = new AtomicBoolean(false);
private final List sourceUpdatedPending = new CopyOnWriteArrayList<>();
private final AtomicBoolean stopRequested = new AtomicBoolean(false);
private final Path buildPath;
private final static String RELOADING = "Reloading";
public AutoDeployHandler(StartMojo start, File webappDirectory) {
this.start = start;
this.project = start.getEnvironment().getMavenProject();
this.webappDirectory = webappDirectory;
this.log = start.getLog();
this.executorService = Executors.newSingleThreadExecutor();
this.buildPath = project.getBasedir().toPath().resolve("target");
}
public void stop() {
stopRequested.set(true);
}
public boolean isAlive() {
return !stopRequested.get();
}
@Override
public void run() {
try {
Path rootPath = project.getBasedir().toPath();
this.watchService = FileSystems.getDefault().newWatchService();
rootPath.register(watchService,
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY);
registerAllDirectories(rootPath);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
if (buildReloadTask != null && !buildReloadTask.isDone()) {
buildReloadTask.cancel(true);
}
executorService.shutdown();
} catch (Exception ex) {
log.error(ex);
}
}));
while (isAlive()) {
WatchKey key = watchService.poll(60, TimeUnit.SECONDS);
if (key != null) {
List> filteredEvents = new ArrayList<>();
for (WatchEvent> event : key.pollEvents()) {
Path changed = (Path) event.context();
Path fullPath = ((Path) key.watchable()).resolve(changed);
if (fullPath.startsWith(buildPath)) {
continue;
}
filteredEvents.add(event);
}
if (!filteredEvents.isEmpty()) {
if (buildReloadTask != null && !buildReloadTask.isDone()) {
buildReloadTask.cancel(true);
}
boolean fileDeletedOrRenamed = false;
boolean resourceModified = false;
boolean testClassesModified = false;
boolean rebootRequired = false;
for (WatchEvent> event : filteredEvents) {
WatchEvent.Kind> kind = event.kind();
Path changed = (Path) event.context();
Path fullPath = ((Path) key.watchable()).resolve(changed);
log.debug("Source modified: " + changed + " - " + kind);
Path projectRoot = Paths.get(project.getBasedir().toURI());
Path sourceRoot = projectRoot.resolve("src");
Path mainDirectory = sourceRoot.resolve("main");
Path javaDirectory = mainDirectory.resolve("java");
Path resourcesDirectory = mainDirectory.resolve("resources");
Path testDirectory = sourceRoot.resolve("test");
sourceUpdatedPending.add(new Source(fullPath, kind, fullPath.startsWith(javaDirectory)));
if (fullPath.startsWith(resourcesDirectory)) {
resourceModified = true;
}
if (fullPath.startsWith(testDirectory)) {
testClassesModified = true;
}
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
if (Files.isDirectory(fullPath, LinkOption.NOFOLLOW_LINKS)) {
register(fullPath); // register watch service for newly created dir
}
}
if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
fileDeletedOrRenamed = true;
cleanPending.set(true);
}
if (start.getRebootOnChange().contains(changed.toString())) {
rebootRequired = true;
fileDeletedOrRenamed = true;
cleanPending.set(true);
break;
}
}
log.debug("sourceUpdatedPending: " + sourceUpdatedPending);
if (!sourceUpdatedPending.isEmpty()) {
WebDriverFactory.updateTitle("Building", project, start.getDriver(), log);
log.info("Auto-build started for " + project.getName());
List goalsList = updateGoalsList(fileDeletedOrRenamed, resourceModified, testClassesModified);
log.info("goalsList: " + goalsList);
executeBuildReloadTask(goalsList, rebootRequired);
}
}
key.reset();
}
}
} catch (Exception ex) {
log.error(ex);
if (hasInotifyLimitReachedException(ex)) {
log.error(WATCH_SERVICE_ERROR_MESSAGE);
}
}
}
private boolean hasInotifyLimitReachedException(Throwable ex) {
while (ex != null) {
if (ex instanceof IOException && ex.getMessage().contains(INOTIFY_USER_LIMIT_REACHED_MESSAGE)) {
return true;
}
ex = ex.getCause();
}
return false;
}
private void registerAllDirectories(Path path) throws IOException {
Files.walk(path)
.filter(Files::isDirectory)
.forEach(this::register);
}
private void register(Path path) {
boolean isChild = path.startsWith(buildPath) && (path.equals(buildPath) || !buildPath.relativize(path).equals(path));
try {
if (!isChild) {
log.debug("register watch service for " + path);
path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
}
} catch (IOException ex) {
log.error("Error registering directories", ex);
}
}
private List updateGoalsList(boolean fileDeletedOrRenamed, boolean resourceModified,
boolean testClassesModified) {
boolean onlyJavaFilesUpdated = sourceUpdatedPending.stream().allMatch(k -> k.path.toString().endsWith(".java") && k.kind == ENTRY_MODIFY && k.javaClass);
List goalsList = new ArrayList<>();
if (fileDeletedOrRenamed || cleanPending.get() || sourceUpdatedPending.size() > 1) {
goalsList.add(0, "clean");
goalsList.add("resources:resources");
resourceModified = true;
} else if (resourceModified) {
goalsList.add("resources:resources");
}
goalsList.add("org.apache.maven.plugins:maven-compiler-plugin:3.12.1:compile"); //v3.12.1 is required as is includes fix https://github.com/apache/maven-compiler-plugin/pull/213
if (onlyJavaFilesUpdated) {
goalsList.add("-Dmaven.compiler.useIncrementalCompilation=false");
}
if (resourceModified || !onlyJavaFilesUpdated) {
goalsList.add("war:exploded");
} else {
Path outputDirectory = Paths.get(webappDirectory.toPath().toString(), "WEB-INF", "classes");
goalsList.add("-Dmaven.compiler.outputDirectory=\"" + outputDirectory.toString() + "\"");
}
if (!testClassesModified) {
goalsList.add("-Dmaven.test.skip=true");
} else {
goalsList.add("-DskipTests");
}
return goalsList;
}
private void executeBuildReloadTask(List goalsList, boolean rebootRequired) {
buildReloadTask = executorService.submit(() -> {
if (goalsList.get(0).equals("clean")) {
deleteBuildDir(project.getBuild().getDirectory());
goalsList.remove(0);
}
InvocationRequest request = new DefaultInvocationRequest();
request.setPomFile(new File(project.getBasedir(), "pom.xml"));
request.setGoals(goalsList);
log.debug("Maven goals: " + goalsList);
System.setProperty("maven.multiModuleProjectDirectory", project.getBasedir().toString());
Invoker invoker = new DefaultInvoker();
invoker.setLogger(new InvokerLoggerImpl(log));
invoker.setInputStream(InputStream.nullInputStream());
try {
InvocationResult result = invoker.execute(request);
if (result.getExitCode() != 0) {
if (!buildReloadTask.isCancelled()) {
log.info("Auto-build failed with exit code: " + result.getExitCode());
WebDriverFactory.updateTitle("Build failed", project, start.getDriver(), log);
}
} else {
log.info("Auto-build successful for " + project.getName());
cleanPending.set(false);
sourceUpdatedPending.clear();
if (rebootRequired) {
if (start.getMicroProcess().isAlive()) {
WebDriverFactory.updateTitle("Restarting", project, start.getDriver(), log);
start.getMicroProcess().destroy();
}
} else {
WebDriverFactory.updateTitle(RELOADING, project, start.getDriver(), log);
ReloadMojo reloadMojo = new ReloadMojo(project, log);
reloadMojo.setDevMode(true);
if(start.contextRoot != null) {
reloadMojo.setContextRoot(start.contextRoot);
}
reloadMojo.setKeepState(start.keepState);
if (start.hotDeploy) {
Path rootPath = project.getBasedir().toPath();
List sourcesChanged = new ArrayList<>();
reloadMojo.setHotDeploy(start.hotDeploy);
for (Source source : sourceUpdatedPending) {
String extension = source.path.toString().substring(source.path.toString().lastIndexOf('.') + 1);
if (extension.equals("xml") || extension.equals("properties")) {
reloadMojo.setMetadataChanged(true);
}
Path relativePath = rootPath.relativize(source.path);
sourcesChanged.add(relativePath.toString().replace(File.separator, "/"));
}
log.debug("SourcesChanged: " + sourcesChanged);
reloadMojo.setSourcesChanged(String.join(", ", sourcesChanged));
}
try {
reloadMojo.execute();
} catch (MojoExecutionException ex) {
log.error("Error invoking Reload", ex);
}
}
cleanPending.set(false);
sourceUpdatedPending.clear();
}
} catch (MavenInvocationException ex) {
log.error("Error invoking Maven", ex);
}
});
}
public void deleteBuildDir(String filePath) {
try {
Path fileToDelete = Paths.get(filePath);
Files.walkFileTree(fileToDelete, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new DeleteFileVisitor());
} catch (IOException e) {
log.error("Error occurred while deleting the file: ", e);
}
}
class DeleteFileVisitor extends SimpleFileVisitor {
private boolean hasJarExtension(Path file) {
return file.getFileName().toString().toLowerCase().endsWith(".jar");
}
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
if (Files.isRegularFile(path)) {
if (hasJarExtension(path)) {
try {
Files.delete(path);
} catch (IOException e) {
// Ignore locked jar
}
} else {
try {
Files.delete(path);
} catch (IOException e) {
log.error("Error occurred while deleting the file: ", e);
}
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
try {
Files.delete(file);
} catch (NoSuchFileException e) {
log.debug("Error occurred while deleting the file: ", e);
} catch (IOException e) {
log.error("Error occurred while deleting the file: ", e);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
try {
Files.delete(dir);
} catch (java.nio.file.DirectoryNotEmptyException e) {
// Ignore
} catch (IOException e) {
log.error("Error occurred while deleting the directory: ", e);
}
return FileVisitResult.CONTINUE;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy