MEECROWAVE-207 basic reload command in run mojo
diff --git a/integration-tests/beanvalidation/pom.xml b/integration-tests/beanvalidation/pom.xml
index 3d0c7a9..1c3323a 100644
--- a/integration-tests/beanvalidation/pom.xml
+++ b/integration-tests/beanvalidation/pom.xml
@@ -53,6 +53,19 @@
     </dependency>
   </dependencies>
 
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.meecrowave</groupId>
+        <artifactId>meecrowave-maven-plugin</artifactId>
+        <version>${project.version}</version>
+        <configuration>
+          <scanningPackageIncludes>org.apache.meecrowave.beanvalidation</scanningPackageIncludes>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
   <profiles>
     <profile>
       <id>java11</id>
diff --git a/meecrowave-doc/src/main/jbake/content/meecrowave-maven/index.adoc b/meecrowave-doc/src/main/jbake/content/meecrowave-maven/index.adoc
index f82b7b2..132f99b 100755
--- a/meecrowave-doc/src/main/jbake/content/meecrowave-maven/index.adoc
+++ b/meecrowave-doc/src/main/jbake/content/meecrowave-maven/index.adoc
@@ -24,6 +24,14 @@
 
 include::../../../../../target/generated-doc/MavenConfiguration.adoc[]
 
+== Run
+
+`mvn meecrowave:run` enables you to start a server configured in your `pom.xml`.
+Once started, you have a few commands you can use to interact with the server:
+
+- `quit`/`exit`: shutdown properly the server.
+- `reload` (since 1.2.9): optionally relaunch a maven compilation - see configuration - and reload the web context.
+
 == Bundling
 
 [source]
diff --git a/meecrowave-maven-plugin/pom.xml b/meecrowave-maven-plugin/pom.xml
index 73dc2cd..afc7dc8 100644
--- a/meecrowave-maven-plugin/pom.xml
+++ b/meecrowave-maven-plugin/pom.xml
@@ -30,7 +30,7 @@
   <packaging>maven-plugin</packaging>
 
   <properties>
-    <maven.version>3.3.9</maven.version>
+    <maven.version>3.6.0</maven.version>
     <meecrowave.build.name>${project.groupId}.maven</meecrowave.build.name>
   </properties>
 
diff --git a/meecrowave-maven-plugin/src/main/java/org/apache/meecrowave/maven/MeecrowaveRunMojo.java b/meecrowave-maven-plugin/src/main/java/org/apache/meecrowave/maven/MeecrowaveRunMojo.java
index 427e2ff..0de0a53 100644
--- a/meecrowave-maven-plugin/src/main/java/org/apache/meecrowave/maven/MeecrowaveRunMojo.java
+++ b/meecrowave-maven-plugin/src/main/java/org/apache/meecrowave/maven/MeecrowaveRunMojo.java
@@ -18,20 +18,14 @@
  */
 package org.apache.meecrowave.maven;
 
-import org.apache.logging.log4j.LogManager;
-import org.apache.maven.artifact.Artifact;
-import org.apache.maven.plugin.AbstractMojo;
-import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.MojoFailureException;
-import org.apache.maven.plugins.annotations.Mojo;
-import org.apache.maven.plugins.annotations.Parameter;
-import org.apache.maven.project.MavenProject;
-import org.apache.meecrowave.Meecrowave;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+import static org.apache.maven.plugins.annotations.ResolutionScope.RUNTIME_PLUS_SYSTEM;
 
-import javax.script.ScriptEngine;
-import javax.script.ScriptEngineManager;
-import javax.script.ScriptException;
-import javax.script.SimpleBindings;
 import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
@@ -47,14 +41,25 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Scanner;
+import java.util.function.Supplier;
 
-import static java.util.Collections.singletonList;
-import static java.util.Collections.singletonMap;
-import static java.util.Optional.ofNullable;
-import static java.util.function.Function.identity;
-import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toMap;
-import static org.apache.maven.plugins.annotations.ResolutionScope.RUNTIME_PLUS_SYSTEM;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+
+import org.apache.catalina.Context;
+import org.apache.logging.log4j.LogManager;
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.lifecycle.internal.LifecycleStarter;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+import org.apache.meecrowave.Meecrowave;
+import org.apache.meecrowave.tomcat.ProvidedLoader;
 
 @Mojo(name = "run", requiresDependencyResolution = RUNTIME_PLUS_SYSTEM)
 public class MeecrowaveRunMojo extends AbstractMojo {
@@ -214,6 +219,9 @@
     @Parameter(defaultValue = "${project.build.outputDirectory}")
     private List<File> modules;
 
+    @Parameter(defaultValue = "${session}", readonly = true)
+    private MavenSession session;
+
     @Parameter(defaultValue = "${project}", readonly = true, required = true)
     private MavenProject project;
 
@@ -299,6 +307,9 @@
     @Parameter(property = "meecrowave.jaxws-support", defaultValue = "true")
     private boolean jaxwsSupportIfAvailable;
 
+    @Parameter(property = "meecrowave.reload-goals", defaultValue = "process-classes")
+    private List<String> reloadGoals; // todo: add watching on project.build.directory?
+
     @Parameter(property = "meecrowave.default-ssl-hostconfig-name")
     private String defaultSSLHostConfigName;
 
@@ -308,12 +319,17 @@
     @Parameter(property = "meecrowave.session-cookie-config")
     private String webSessionCookieConfig;
 
+    @Component
+    private LifecycleStarter lifecycleStarter;
+
     @Override
-    public void execute() throws MojoExecutionException, MojoFailureException {
+    public void execute() {
         if (skip) {
             getLog().warn("Mojo skipped");
             return;
         }
+        logConfigurationErrors();
+
         final Map<String, String> originalSystemProps;
         if (systemProperties != null) {
             originalSystemProps = systemProperties.keySet().stream()
@@ -326,8 +342,8 @@
 
         final Thread thread = Thread.currentThread();
         final ClassLoader loader = thread.getContextClassLoader();
-        final ClassLoader appLoader = createClassLoader(loader);
-        thread.setContextClassLoader(appLoader);
+        final Supplier<ClassLoader> appLoaderSupplier = createClassLoader(loader);
+        thread.setContextClassLoader(appLoaderSupplier.get());
         try {
             final Meecrowave.Builder builder = getConfig();
             try (final Meecrowave meecrowave = new Meecrowave(builder) {
@@ -344,24 +360,34 @@
                         jsContextCustomizer == null ?
                                 null : ctx -> scriptCustomization(
                                 singletonList(jsContextCustomizer), "js", singletonMap("context", ctx)));
-                if (useClasspathDeployment) {
-                    meecrowave.deployClasspath(deploymentMeta);
-                } else {
-                    meecrowave.deployWebapp(deploymentMeta);
+                deploy(meecrowave, deploymentMeta);
+                final Scanner scanner = new Scanner(System.in);
+                String cmd;
+                boolean quit = false;
+                while (!quit && (cmd = scanner.next()) != null) {
+                    cmd = cmd.trim();
+                    switch (cmd) {
+                        case "": // normally impossible with a Scanner but we can move to another "reader"
+                        case "q":
+                        case "quit":
+                        case "e":
+                        case "exit":
+                            quit = true;
+                            break;
+                        case "r":
+                        case "reload":
+                            reload(meecrowave, fixedContext, appLoaderSupplier, loader);
+                            break;
+                        default:
+                            getLog().error("Unknown command: '" + cmd + "', use 'quit' or 'exit' or 'reload'");
+                    }
                 }
-                new Scanner(System.in).next();
             }
         } finally {
             if (forceLog4j2Shutdown) {
                 LogManager.shutdown();
             }
-            if (appLoader != loader) {
-                try {
-                    URLClassLoader.class.cast(appLoader).close();
-                } catch (final IOException e) {
-                    getLog().warn(e.getMessage(), e);
-                }
-            }
+            destroyTcclIfNeeded(thread, loader);
             thread.setContextClassLoader(loader);
             if (originalSystemProps != null) {
                 systemProperties.keySet().forEach(k -> {
@@ -376,6 +402,51 @@
         }
     }
 
+    private void destroyTcclIfNeeded(final Thread thread, final ClassLoader loader) {
+        if (thread.getContextClassLoader() != loader) {
+            try {
+                URLClassLoader.class.cast(thread.getContextClassLoader()).close();
+            } catch (final IOException e) {
+                getLog().warn(e.getMessage(), e);
+            }
+        }
+    }
+
+    private void logConfigurationErrors() {
+        if (watcherBouncing > 0 && reloadGoals != null && !reloadGoals.isEmpty()) {
+            getLog().warn("You set reloadGoals and watcherBouncing > 1, behavior is undefined");
+        }
+    }
+
+    private void reload(final Meecrowave meecrowave, final String context,
+                        final Supplier<ClassLoader> loaderSupplier, final ClassLoader mojoLoader) {
+        if (reloadGoals != null && !reloadGoals.isEmpty()) {
+            final List<String> goals = session.getGoals();
+            session.getRequest().setGoals(reloadGoals);
+            try {
+                lifecycleStarter.execute(session);
+            } finally {
+                session.getRequest().setGoals(goals);
+            }
+        }
+        final Context ctx = Context.class.cast(meecrowave.getTomcat().getHost().findChild(context));
+        if (useClasspathDeployment) {
+            final Thread thread = Thread.currentThread();
+            destroyTcclIfNeeded(thread, mojoLoader);
+            thread.setContextClassLoader(loaderSupplier.get());
+            ctx.setLoader(new ProvidedLoader(thread.getContextClassLoader(), meecrowave.getConfiguration().isTomcatWrapLoader()));
+        }
+        ctx.reload();
+    }
+
+    private void deploy(final Meecrowave meecrowave, final Meecrowave.DeploymentMeta deploymentMeta) {
+        if (useClasspathDeployment) {
+            meecrowave.deployClasspath(deploymentMeta);
+        } else {
+            meecrowave.deployWebapp(deploymentMeta);
+        }
+    }
+
     private void scriptCustomization(final List<String> customizers, final String ext, final Map<String, Object> customBindings) {
         if (customizers == null || customizers.isEmpty()) {
             return;
@@ -396,7 +467,7 @@
         }
     }
 
-    private ClassLoader createClassLoader(final ClassLoader parent) {
+    private Supplier<ClassLoader> createClassLoader(final ClassLoader parent) {
         final List<URL> urls = new ArrayList<>();
         urls.addAll(project.getArtifacts().stream()
                 .filter(a -> !((applicationScopes == null && !(Artifact.SCOPE_COMPILE.equals(a.getScope()) || Artifact.SCOPE_RUNTIME.equals(a.getScope())))
@@ -416,7 +487,7 @@
                 throw new IllegalArgumentException(e);
             }
         }).collect(toList()));
-        return urls.isEmpty() ? parent : new URLClassLoader(urls.toArray(new URL[urls.size()]), parent) {
+        return urls.isEmpty() ? () -> parent : () -> new URLClassLoader(urls.toArray(new URL[0]), parent) {
             @Override
             public boolean equals(final Object obj) {
                 return super.equals(obj) || parent.equals(obj);
diff --git a/meecrowave-maven-plugin/src/test/java/org/apache/meecrowave/maven/MeecrowaveRunMojoTest.java b/meecrowave-maven-plugin/src/test/java/org/apache/meecrowave/maven/MeecrowaveRunMojoTest.java
index 730911f..84b2dc0 100644
--- a/meecrowave-maven-plugin/src/test/java/org/apache/meecrowave/maven/MeecrowaveRunMojoTest.java
+++ b/meecrowave-maven-plugin/src/test/java/org/apache/meecrowave/maven/MeecrowaveRunMojoTest.java
@@ -34,11 +34,13 @@
 import org.junit.Rule;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.ServerSocket;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -76,7 +78,7 @@
         final InputStream in = System.in;
         final CountDownLatch latch = new CountDownLatch(1);
         System.setIn(new InputStream() {
-            private int val = 2; // just to not return nothing
+            private final InputStream delegate = new ByteArrayInputStream("quit".getBytes(StandardCharsets.UTF_8));
 
             @Override
             public int read() throws IOException {
@@ -86,7 +88,7 @@
                     Thread.currentThread().interrupt();
                     fail(e.getMessage());
                 }
-                return val--;
+                return delegate.read();
             }
         });
         final Thread runner = new Thread() {