Java tutorial
/* * * Copyright (c) 2016-2017 Red Hat, Inc. * * Red Hat 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 io.reactiverse.vertx.maven.plugin.mojos; import io.reactiverse.vertx.maven.plugin.utils.*; import org.apache.commons.lang3.ClassUtils; import org.apache.maven.model.Resource; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Parameter; import org.codehaus.plexus.util.DirectoryScanner; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; public class AbstractRunMojo extends AbstractVertxMojo { /* ==== Maven related ==== */ /** * The maven project classes directory, defaults to target/classes */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) protected File classesDirectory; /** * This property is used to enable vertx to do redeployment of the verticles in case of modifications * to the sources. * The redeployPattern defines the source directories that will be watched for changes to trigger redeployment */ @Parameter(name = "redeploy") protected boolean redeploy; /** * */ @Parameter(alias = "redeployScanPeriod", property = "vertx.redeploy.scan.period", defaultValue = "1000") long redeployScanPeriod; /** * */ @Parameter(alias = "redeployGracePeriod", property = "vertx.redeploy.grace.period") long redeployGracePeriod; /** * */ @Parameter(alias = "redeployTerminationPeriod", property = "vertx.redeploy.termination.period", defaultValue = "1000") long redeployTerminationPeriod; /** * The default command to use when calling io.vertx.core.Launcher. * possible commands are, * <ul> * <li>bare</li> * <li>list</li> * <li>run</li> * <li>start</li> * <li>stop</li> * <li>run</li> * </ul> */ protected String vertxCommand = "run"; /** * This property will be passed as the -config option to vertx run. It defaults to file * "src/main/config/application.json", if it exists it will passed to the vertx run */ @Parameter(alias = "config", property = "vertx.config", defaultValue = "src/main/conf/application.json") File config; /** * This property will be used as the working directory for the process when running in forked mode. * This defaults to ${project.basedir} */ @Parameter(alias = "workDirectory", property = "vertx.directory", defaultValue = "${project.basedir}") File workDirectory; /** * JVM arguments that should be associated with the forked process used to run the * application. On command line, make sure to wrap multiple values between quotes. * <p> * The additional arguments that will be passed as program arguments to the JVM, all standard vertx arguments are * automatically applied. * <p> * * @since 1.0.2 */ @Parameter(alias = "jvmArgs", property = "vertx.jvmArguments") protected List<String> jvmArgs; /** * to hold extra options that can be passed to run command */ protected List<String> optionalRunExtraArgs; @Override public void execute() throws MojoExecutionException, MojoFailureException { if (skip) { getLog().info("vertx:run skipped by configuration"); return; } compileIfNeeded(); List<String> argsList = new ArrayList<>(); scanAndLoadConfigs(); boolean isVertxLauncher = isVertxLauncher(launcher); getLog().info("Launching Vert.x Application"); if (isVertxLauncher) { addVertxArgs(argsList); } else if (redeploy) { getLog().info("Vert.x application redeploy enabled"); argsList.add(0, IO_VERTX_CORE_LAUNCHER); argsList.add(1, "run"); StringBuilder redeployArg = new StringBuilder(); redeployArg.append(VERTX_ARG_REDEPLOY); //fix for redeploy to work computeOutputDirsWildcard(redeployArg); argsList.add(redeployArg.toString()); addRedeployExtraArgs(argsList); argsList.add(VERTX_ARG_LAUNCHER_CLASS); argsList.add(launcher); if (jvmArgs != null && !jvmArgs.isEmpty()) { String javaOpts = jvmArgs.stream().collect(Collectors.joining(" ")); String argJavaOpts = VERTX_ARG_JAVA_OPT + "=" + javaOpts; argsList.add(argJavaOpts); } } else { argsList.add(launcher); } addRunExtraArgs(argsList); run(argsList); } private void compileIfNeeded() { File classes = new File(project.getBuild().getOutputDirectory()); if (!classes.isDirectory()) { MavenExecutionUtils.execute("compile", project, mavenSession, lifecycleExecutor, container); } } /** * This add or build the classpath that will be passed to the forked process JVM i.e "-cp" * * @param args - the forked process argument list to which the classpath will be appended * @throws MojoExecutionException - any error that might occur while building or adding classpath */ protected void addClasspath(List<String> args) throws MojoExecutionException { try { StringBuilder classpath = new StringBuilder(); for (URL ele : getClassPathUrls()) { classpath = classpath.append(classpath.length() > 0 ? File.pathSeparator : "") .append(new File(ele.toURI())); } getLog().debug("Classpath for forked process: " + classpath); args.add("-cp"); args.add(classpath.toString()); } catch (Exception ex) { throw new MojoExecutionException("Could not build classpath", ex); } } /** * This will add the ${project.build.outputDirectory} to the classpath url collection * * @param classpathUrls - the existing classpath url collection to which the ${project.build.outputDirectory} be added * @throws IOException - any exception that might occur while get the classes directory as URL */ protected void addClassesDirectory(List<URL> classpathUrls) throws IOException { classpathUrls.add(this.classesDirectory.toURI().toURL()); } /** * This will build the Vertx specific arguments that needs to be passed to the runnable process * * @param argsList - the existing collection of arguments to which the vertx arguments will be added */ protected void addVertxArgs(List<String> argsList) { Objects.requireNonNull(launcher); if (IO_VERTX_CORE_LAUNCHER.equals(launcher)) { argsList.add(IO_VERTX_CORE_LAUNCHER); } else { argsList.add(launcher); } argsList.add(vertxCommand); //Since Verticles will be deployed from custom launchers we dont pass this as argument if (verticle != null && !VERTX_COMMAND_STOP.equals(vertxCommand)) { argsList.add(verticle); } if (redeploy && !(VERTX_COMMAND_START.equals(vertxCommand) || VERTX_COMMAND_STOP.equals(vertxCommand))) { getLog().info("Vert.x application redeploy enabled"); StringBuilder redeployArg = new StringBuilder(); redeployArg.append(VERTX_ARG_REDEPLOY); //fix for redeploy to work computeOutputDirsWildcard(redeployArg); argsList.add(redeployArg.toString()); addRedeployExtraArgs(argsList); if (jvmArgs != null && !jvmArgs.isEmpty()) { String javaOpts = jvmArgs.stream().collect(Collectors.joining(" ")); argsList.add(VERTX_ARG_JAVA_OPT + "=" + javaOpts); } } if (!VERTX_COMMAND_STOP.equals(vertxCommand)) { String argLauncherClass = VERTX_ARG_LAUNCHER_CLASS + "=\"" + launcher + "\""; argsList.add(argLauncherClass); if (config != null && config.exists() && config.isFile()) { getLog().info("Using configuration from file: " + config.toString()); argsList.add(VERTX_ARG_CONF); argsList.add(config.toString()); } } } /** * The method that will compute the Output directory wildcard that will be added to the * --redeploy argument to Vert.x to trigger redeployment for scanning for changes to trigger * redeployment * * @param redeployArg - the redeploy {@link StringBuilder} to which the values will be appended */ private void computeOutputDirsWildcard(StringBuilder redeployArg) { final String wildcardClassesDir = this.classesDirectory.toString() + "/**/*"; redeployArg.append(wildcardClassesDir); } /** * This method will add the extra redeployment arguments as mentioned in * * @param argsList - the argument list to be appended * @see <a href="http://vertx.io/docs/vertx-core/java/#_live_redeploy">Live Redeploy</a> */ private void addRedeployExtraArgs(List<String> argsList) { if (redeployScanPeriod > 0) { argsList.add(VERTX_ARG_REDEPLOY_SCAN_PERIOD + redeployScanPeriod); } if (redeployGracePeriod > 0) { argsList.add(VERTX_ARG_REDEPLOY_GRACE_PERIOD + redeployGracePeriod); } if (redeployTerminationPeriod > 0) { argsList.add(VERTX_ARG_REDEPLOY_TERMINATION_PERIOD + redeployTerminationPeriod); } } /** * This method will add the extra arguments required for the run either used by core Vert.x Launcher * or by custom Launcher * @param argsList */ private void addRunExtraArgs(List<String> argsList) { if ("run".equals(vertxCommand)) { if (optionalRunExtraArgs != null && !optionalRunExtraArgs.isEmpty()) { argsList.addAll(optionalRunExtraArgs); } } } /** * Method to check if the Launcher is {@link AbstractRunMojo#IO_VERTX_CORE_LAUNCHER} or instance of * {@code io.vertx.core.Launcher} * * @param launcher - the launcher class as string that needs to be checked * @return true if its {@link AbstractRunMojo#IO_VERTX_CORE_LAUNCHER} or instance of * {@code io.vertx.core.Launcher} * @throws MojoExecutionException - any error that might occur while checking */ protected boolean isVertxLauncher(String launcher) throws MojoExecutionException { if (launcher != null) { if (IO_VERTX_CORE_LAUNCHER.equals(launcher)) { return true; } else { try { Class customLauncher = buildClassLoader(getClassPathUrls()).loadClass(launcher); List<Class<?>> superClasses = ClassUtils.getAllSuperclasses(customLauncher); boolean isAssignable = false; if (superClasses != null) { for (Class<?> superClass : superClasses) { if (IO_VERTX_CORE_LAUNCHER.equals(superClass.getName())) { isAssignable = true; break; } } } return isAssignable; } catch (ClassNotFoundException e) { throw new MojoExecutionException("Class \"" + launcher + "\" not found"); } } } else { return false; } } /** * This method will trigger the lauch of the applicaiton as non-forked, running in same JVM as maven. * * @param argsList - the arguments to be passed to the vertx launcher * @throws MojoExecutionException - any error that might occur while starting the process */ protected void run(List<String> argsList) throws MojoExecutionException { JavaProcessExecutor vertxExecutor = new JavaProcessExecutor() .withJvmOpts(redeploy ? Collections.emptyList() : jvmArgs).withArgs(argsList) .withClassPath(getClassPathUrls()).withLogger(getLog()).withWaitFor(true); try { //When redeploy is enabled spin up the Incremental builder in background if (redeploy && !(VERTX_COMMAND_START.equals(vertxCommand) || VERTX_COMMAND_STOP.equals(vertxCommand))) { getLog().debug("Collected mojos: " + MojoSpy.MOJOS); Set<Path> inclDirs = Collections.singleton(new File(project.getBasedir(), "src/main").toPath()); //TODO - handle exceptions effectively // TODO - Not sure about the runAsync here, it uses the default fork join pool CompletableFuture.runAsync(() -> { List<Callable<Void>> chain = computeExecutionChain(); IncrementalBuilder incrementalBuilder = new IncrementalBuilder(inclDirs, chain, getLog(), redeployScanPeriod); incrementalBuilder.run(); }); } vertxExecutor.execute(); } catch (Exception e) { throw new MojoExecutionException("Unable to launch incremental builder", e); } } private List<Callable<Void>> computeExecutionChain() { List<Callable<Void>> list = new ArrayList<>(); if (MojoSpy.MOJOS.isEmpty()) { getLog().info("No plugin execution collected. The vertx:initialize goal has not " + "been run beforehand. Only handling resources and java compilation"); list.add(new JavaBuildCallback()); list.add(new ResourceBuildCallback()); } else { list = MojoSpy.MOJOS.stream() // Include only mojo in [generate-source, process-classes] .filter(exec -> MojoSpy.PHASES.contains(exec.getLifecyclePhase())).map(this::toTask) .collect(Collectors.toList()); } return list; } private Callable<Void> toTask(MojoExecution execution) { MojoExecutor executor = new MojoExecutor(execution, project, mavenSession, buildPluginManager); return () -> { try { //--- vertx-maven-plugin:1.0-SNAPSHOT:run (default-cli) @ vertx-demo getLog().info(">>> " + execution.getArtifactId() + ":" + execution.getVersion() + ":" + execution.getGoal() + " (" + execution.getExecutionId() + ") @" + project.getArtifactId()); executor.execute(); } catch (Exception e) { getLog().error("Error while doing incremental build", e); } return null; }; } /** * This method to load Vert.X application configurations. * This will use the pattern ${basedir}/src/main/conf/application.[json/yaml/yml] */ protected void scanAndLoadConfigs() throws MojoExecutionException { Path confBaseDir = Paths.get(this.project.getBasedir().toString(), "src", "main", "conf"); if (Files.exists(confBaseDir) && Files.isDirectory(confBaseDir)) { DirectoryScanner directoryScanner = new DirectoryScanner(); directoryScanner.setBasedir(this.project.getBasedir() + DEFAULT_CONF_DIR); directoryScanner.setIncludes(WILDCARD_CONFIG_FILES); directoryScanner.scan(); String[] configFiles = directoryScanner.getIncludedFiles(); if (configFiles != null && configFiles.length != 0) { String configFile = configFiles[0]; Path confPath = Paths.get(confBaseDir.toFile().toString(), configFile); //Check if its JSON if (isJson(configFile)) { config = confPath.toFile(); } else if (isYaml(configFile)) { //Check if its YAML or YML Path jsonConfDir = Paths.get(this.projectBuildDir, "conf"); jsonConfDir.toFile().mkdirs(); Path jsonConfPath = Paths.get(jsonConfDir.toString(), VERTX_CONFIG_FILE_JSON); try { Files.deleteIfExists(jsonConfPath); if (Files.createFile(jsonConfPath).toFile().exists()) { ConfigConverterUtil.convertYamlToJson(confPath, jsonConfPath); config = jsonConfPath.toFile(); } } catch (IOException e) { throw new MojoExecutionException("Error loading configuration file:" + confPath.toString(), e); } catch (Exception e) { throw new MojoExecutionException( "Error loading and converting configuration file:" + confPath.toString(), e); } } } } } /** * This will build the {@link URLClassLoader} object from the collection of classpath URLS * * @param classPathUrls - the classpath urls which will be used to build the {@link URLClassLoader} * @return an instance of {@link URLClassLoader} */ protected ClassLoader buildClassLoader(Collection<URL> classPathUrls) { return new URLClassLoader(classPathUrls.toArray(new URL[classPathUrls.size()])); } /** * This will resolve the project's test and runtime dependencies along with classes directory to the collection * of classpath urls. Notice that resources directory are NOT appended, as they should be copied to tha * `target/classes` directory. * * @return @{link {@link List<URL>}} which will have all the dependencies, classes directory, resources directory etc., * @throws MojoExecutionException any error that might occur while building collection like resolution errors */ protected List<URL> getClassPathUrls() throws MojoExecutionException { List<URL> classPathUrls = new ArrayList<>(); try { addClassesDirectory(classPathUrls); Set<Optional<File>> compileAndRuntimeDeps = extractArtifactPaths(this.project.getDependencyArtifacts()); Set<Optional<File>> transitiveDeps = extractArtifactPaths(this.project.getArtifacts()); classPathUrls.addAll(Stream.concat(compileAndRuntimeDeps.stream(), transitiveDeps.stream()) .filter(Optional::isPresent).map(file -> { try { return file.get().toURI().toURL(); } catch (Exception e) { getLog().error("Error building classpath", e); } return null; }).filter(Objects::nonNull).collect(Collectors.toList())); } catch (IOException e) { throw new MojoExecutionException("Unable to run:", e); } return classPathUrls; } /** * Method to check if the file is JSON file * * @param configFile - the config file to be checked * @return if its json file e.g. application.json */ private boolean isJson(String configFile) { return configFile != null && configFile.endsWith(".json"); } /** * Method to check if the file is YAML file * * @param configFile - the config file to be checked * @return if its YAML file e.g. application.yml or application.yml */ private boolean isYaml(String configFile) { return configFile != null && (configFile.endsWith(".yaml") || configFile.endsWith(".yml")); } /** * */ public final class JavaBuildCallback implements Callable<Void> { @Override public Void call() { try { MojoUtils.compile(project, mavenSession, buildPluginManager); } catch (Exception e) { getLog().error("Error while doing incremental Java build: " + e.getMessage(), e); } return null; } } /** * */ public final class ResourceBuildCallback implements Callable<Void> { @Override public Void call() { try { MojoUtils.copyResources(project, mavenSession, buildPluginManager); } catch (Exception e) { getLog().error("Error while doing incremental resource processing: " + e.getMessage(), e); } return null; } } }