blob: 0de0a532691768cb5173a1cce9fa51a2a92653f3 [file] [log] [blame]
/*
* 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.apache.meecrowave.maven;
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 java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.function.Supplier;
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 {
@Parameter(property = "meecrowave.watcher-bounding", defaultValue = "0")
private int watcherBouncing;
@Parameter(property = "meecrowave.http", defaultValue = "8080")
private int httpPort;
@Parameter(property = "meecrowave.https", defaultValue = "8443")
private int httpsPort;
@Parameter(property = "meecrowave.stop", defaultValue = "8005")
private int stopPort;
@Parameter(property = "meecrowave.host", defaultValue = "localhost")
private String host;
@Parameter(property = "meecrowave.dir")
protected String dir;
@Parameter(property = "meecrowave.serverXml")
private File serverXml;
@Parameter(property = "meecrowave.keepServerXmlAsThis")
private boolean keepServerXmlAsThis;
@Parameter(property = "meecrowave.jaxrsLogProviders", defaultValue = "false")
private boolean jaxrsLogProviders;
@Parameter(property = "meecrowave.tomcatWrapLoader", defaultValue = "false")
private boolean tomcatWrapLoader;
@Parameter(property = "meecrowave.useTomcatDefaults", defaultValue = "true")
private boolean useTomcatDefaults;
@Parameter(property = "meecrowave.antiResourceLocking", defaultValue = "false")
private boolean antiResourceLocking;
@Parameter
private Map<String, String> properties;
@Parameter
private Map<String, String> systemProperties;
@Parameter
private Map<String, String> cxfServletParams;
@Parameter(property = "meecrowave.tomcatNoJmx", defaultValue = "true")
private boolean tomcatNoJmx;
@Parameter(property = "meecrowave.quickSession", defaultValue = "true")
private boolean quickSession;
@Parameter(property = "meecrowave.tomcatScanning", defaultValue = "true")
private boolean tomcatScanning;
@Parameter(property = "meecrowave.tomcatAutoSetup", defaultValue = "true")
private boolean tomcatAutoSetup;
@Parameter(property = "meecrowave.tomcatJspDevelopment", defaultValue = "false")
private boolean tomcatJspDevelopment;
@Parameter(property = "meecrowave.skipHttp")
private boolean skipHttp;
@Parameter(property = "meecrowave.ssl")
private boolean ssl;
@Parameter(property = "meecrowave.keystoreFile")
private String keystoreFile;
@Parameter(property = "meecrowave.keystorePass")
private String keystorePass;
@Parameter(property = "meecrowave.keystoreType", defaultValue = "JKS")
private String keystoreType;
@Parameter(property = "meecrowave.clientAuth")
private String clientAuth;
@Parameter(property = "meecrowave.keyAlias")
private String keyAlias;
@Parameter(property = "meecrowave.sslProtocol")
private String sslProtocol;
@Parameter(property = "meecrowave.webXml")
private String webXml;
@Parameter(property = "meecrowave.tomcatAccessLogPattern")
private String tomcatAccessLogPattern;
@Parameter
private Meecrowave.LoginConfigBuilder loginConfig;
@Parameter
private Collection<Meecrowave.SecurityConstaintBuilder> securityConstraints = new LinkedList<>();
@Parameter
private Map<String, String> users;
@Parameter
private Map<String, String> roles;
@Parameter(property = "meecrowave.http2")
private boolean http2;
@Parameter(property = "meecrowave.tempDir")
private String tempDir;
@Parameter(property = "meecrowave.webResourceCached", defaultValue = "true")
private boolean webResourceCached;
@Parameter(property = "meecrowave.conf")
private String conf;
@Parameter(property = "meecrowave.deleteBaseOnStartup", defaultValue = "true")
private boolean deleteBaseOnStartup;
@Parameter(property = "meecrowave.jaxrsMapping", defaultValue = "/*")
private String jaxrsMapping;
@Parameter(property = "meecrowave.cdiConversation", defaultValue = "false")
private boolean cdiConversation;
@Parameter(property = "meecrowave.skip")
private boolean skip;
@Parameter(property = "meecrowave.jaxrs-beanvalidation", defaultValue = "true")
private boolean jaxrsAutoActivateBeanValidation;
@Parameter(property = "meecrowave.jaxrs-default-providers")
private String jaxrsDefaultProviders;
@Parameter(property = "meecrowave.jaxrs-provider-setup", defaultValue = "true")
private boolean jaxrsProviderSetup;
@Parameter(property = "meecrowave.logging-global-setup", defaultValue = "true")
private boolean loggingGlobalSetup;
@Parameter(property = "meecrowave.servlet-container-initializer-injections", defaultValue = "true")
private boolean injectServletContainerInitializer;
@Parameter(property = "meecrowave.shutdown-hook", defaultValue = "true")
private boolean useShutdownHook;
@Parameter(property = "meecrowave.initialiaze-client-bus", defaultValue = "true")
private boolean initializeClientBus;
@Parameter
private List<String> jsCustomizers;
@Parameter
private List<String> applicationScopes;
@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;
@Parameter(property = "meecrowave.tomcatFilter")
private String tomcatFilter;
@Parameter(property = "meecrowave.context", defaultValue = "")
private String context;
// we don't need to resolve from maven coordinates cause can be added to the plugin deps, just here to reproduce manual deployments
@Parameter(property = "meecrowave.shared-libraries")
private String sharedLibraries;
@Parameter(property = "meecrowave.log4j2-jul-bridge", defaultValue = "true")
private boolean useLog4j2JulLogManager;
@Parameter(property = "meecrowave.jsonp-buffer-strategy", defaultValue = "QUEUE")
private String jsonpBufferStrategy;
@Parameter(property = "meecrowave.jsonp-max-string-length", defaultValue = "10485760")
private int jsonpMaxStringLen;
@Parameter(property = "meecrowave.jsonp-max-read-buffer-size", defaultValue = "65536")
private int jsonpMaxReadBufferLen;
@Parameter(property = "meecrowave.jsonp-max-write-buffer-size", defaultValue = "65536")
private int jsonpMaxWriteBufferLen;
@Parameter(property = "meecrowave.jsonp-comments", defaultValue = "false")
private boolean jsonpSupportsComment;
@Parameter(property = "meecrowave.jsonp-prettify", defaultValue = "false")
private boolean jsonpPrettify;
@Parameter(property = "meecrowave.jsonb-encoding", defaultValue = "UTF-8")
private String jsonbEncoding;
@Parameter(property = "meecrowave.jsonb-nulls", defaultValue = "false")
private boolean jsonbNulls = false;
@Parameter(property = "meecrowave.jsonb-ijson", defaultValue = "false")
private boolean jsonbIJson;
@Parameter(property = "meecrowave.jsonb-prettify", defaultValue = "false")
private boolean jsonbPrettify;
@Parameter(property = "meecrowave.jsonb-binary-strategy")
private String jsonbBinaryStrategy;
@Parameter(property = "meecrowave.jsonb-naming-strategy")
private String jsonbNamingStrategy;
@Parameter(property = "meecrowave.jsonb-order-strategy")
private String jsonbOrderStrategy;
@Parameter(property = "meecrowave.scanning-include")
private String scanningIncludes;
@Parameter(property = "meecrowave.scanning-exclude")
private String scanningExcludes;
@Parameter(property = "meecrowave.scanning-package-include")
private String scanningPackageIncludes;
@Parameter(property = "meecrowave.scanning-package-exclude")
private String scanningPackageExcludes;
@Parameter(property = "meecrowave.force-log4j2-shutdown", defaultValue = "true")
private boolean forceLog4j2Shutdown;
@Parameter(property = "meecrowave.webapp", defaultValue = "${project.basedir}/src/main/webapp")
private File webapp;
@Parameter(property = "meecrowave.force-classpath-deployment", defaultValue = "true")
private boolean useClasspathDeployment;
@Parameter
private String jsContextCustomizer;
@Parameter(property = "meecrowave.meecrowave-properties", defaultValue = "meecrowave.properties")
private String meecrowaveProperties;
@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;
@Parameter(property = "meecrowave.session-timeout")
private Integer webSessionTimeout;
@Parameter(property = "meecrowave.session-cookie-config")
private String webSessionCookieConfig;
@Component
private LifecycleStarter lifecycleStarter;
@Override
public void execute() {
if (skip) {
getLog().warn("Mojo skipped");
return;
}
logConfigurationErrors();
final Map<String, String> originalSystemProps;
if (systemProperties != null) {
originalSystemProps = systemProperties.keySet().stream()
.filter(System.getProperties()::containsKey)
.collect(toMap(identity(), System::getProperty));
systemProperties.forEach(System::setProperty);
} else {
originalSystemProps = null;
}
final Thread thread = Thread.currentThread();
final ClassLoader loader = thread.getContextClassLoader();
final Supplier<ClassLoader> appLoaderSupplier = createClassLoader(loader);
thread.setContextClassLoader(appLoaderSupplier.get());
try {
final Meecrowave.Builder builder = getConfig();
try (final Meecrowave meecrowave = new Meecrowave(builder) {
@Override
protected void beforeStart() {
scriptCustomization(jsCustomizers, "js", singletonMap("meecrowaveBase", base.getAbsolutePath()));
}
}) {
meecrowave.start();
final String fixedContext = ofNullable(context).orElse("");
final Meecrowave.DeploymentMeta deploymentMeta = new Meecrowave.DeploymentMeta(
fixedContext,
webapp != null && webapp.isDirectory() ? webapp : null,
jsContextCustomizer == null ?
null : ctx -> scriptCustomization(
singletonList(jsContextCustomizer), "js", singletonMap("context", ctx)));
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'");
}
}
}
} finally {
if (forceLog4j2Shutdown) {
LogManager.shutdown();
}
destroyTcclIfNeeded(thread, loader);
thread.setContextClassLoader(loader);
if (originalSystemProps != null) {
systemProperties.keySet().forEach(k -> {
final Optional<String> originalValue = ofNullable(originalSystemProps.get(k));
if (originalValue.isPresent()) {
System.setProperty(k, originalValue.get());
} else {
System.clearProperty(k);
}
});
}
}
}
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;
}
final ScriptEngine engine = new ScriptEngineManager().getEngineByExtension(ext);
if (engine == null) {
throw new IllegalStateException("No engine for " + ext + ". Maybe add the JSR223 implementation as plugin dependency.");
}
for (final String js : customizers) {
try {
final SimpleBindings bindings = new SimpleBindings();
bindings.put("project", project);
engine.eval(new StringReader(js), bindings);
bindings.putAll(customBindings);
} catch (final ScriptException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}
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())))
|| (applicationScopes != null && !applicationScopes.contains(a.getScope()))))
.map(f -> {
try {
return f.getFile().toURI().toURL();
} catch (final MalformedURLException e) {
throw new IllegalArgumentException(e);
}
})
.collect(toList()));
urls.addAll(ofNullable(modules).orElse(Collections.emptyList()).stream().map(f -> {
try {
return f.toURI().toURL();
} catch (final MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}).collect(toList()));
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);
}
};
}
private Meecrowave.Builder getConfig() {
final Meecrowave.Builder config = new Meecrowave.Builder();
for (final Field field : MeecrowaveRunMojo.class.getDeclaredFields()) {
if ("properties".equals(field.getName())) {
continue;
}
try {
final Field configField = Meecrowave.Builder.class.getDeclaredField(field.getName());
field.setAccessible(true);
configField.setAccessible(true);
final Object value = field.get(this);
if (value != null) {
configField.set(config, value);
getLog().debug("using " + field.getName() + " = " + value);
}
} catch (final NoSuchFieldException nsfe) {
// ignored
} catch (final Exception e) {
getLog().warn("can't initialize attribute " + field.getName());
}
}
config.loadFrom(meecrowaveProperties);
if (properties != null) {
config.getProperties().putAll(properties);
}
return config;
}
}