blob: 3dbefdbe0c9781cc8f8e43fb9024a6e7c7920a46 [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;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Comparator.comparing;
import static java.util.Locale.ROOT;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.Writer;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import javax.enterprise.inject.spi.InjectionTarget;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.SessionCookieConfig;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.apache.catalina.Context;
import org.apache.catalina.Globals;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Manager;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Realm;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.session.ManagerBase;
import org.apache.catalina.session.StandardManager;
import org.apache.catalina.startup.Catalina;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.http2.Http2Protocol;
import org.apache.meecrowave.api.StartListening;
import org.apache.meecrowave.api.StopListening;
import org.apache.meecrowave.cxf.ConfigurableBus;
import org.apache.meecrowave.cxf.CxfCdiAutoSetup;
import org.apache.meecrowave.cxf.Cxfs;
import org.apache.meecrowave.io.IO;
import org.apache.meecrowave.lang.Substitutor;
import org.apache.meecrowave.logging.jul.Log4j2Logger;
import org.apache.meecrowave.logging.log4j2.Log4j2Shutdown;
import org.apache.meecrowave.logging.log4j2.Log4j2s;
import org.apache.meecrowave.logging.openwebbeans.Log4j2LoggerFactory;
import org.apache.meecrowave.logging.tomcat.Log4j2Log;
import org.apache.meecrowave.logging.tomcat.LogFacade;
import org.apache.meecrowave.openwebbeans.OWBAutoSetup;
import org.apache.meecrowave.runner.cli.CliOption;
import org.apache.meecrowave.service.ValueTransformer;
import org.apache.meecrowave.tomcat.CDIInstanceManager;
import org.apache.meecrowave.tomcat.LoggingAccessLogPattern;
import org.apache.meecrowave.tomcat.MeecrowaveContextConfig;
import org.apache.meecrowave.tomcat.OWBJarScanner;
import org.apache.meecrowave.tomcat.ProvidedLoader;
import org.apache.meecrowave.tomcat.TomcatAutoInitializer;
import org.apache.tomcat.JarScanFilter;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.webbeans.config.WebBeansContext;
import org.apache.xbean.finder.ResourceFinder;
import org.apache.xbean.recipe.ObjectRecipe;
import org.apache.xbean.recipe.Option;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class Meecrowave implements AutoCloseable {
private final Builder configuration;
protected ConfigurableBus clientBus;
protected File base;
protected final File ownedTempDir;
protected File workDir;
protected InternalTomcat tomcat;
protected volatile Thread hook;
// we can undeploy webapps with that later
private final Map<String, Runnable> contexts = new HashMap<>();
private Runnable postTask;
private boolean clearCatalinaSystemProperties;
private boolean deleteBase;
public Meecrowave() {
this(new Builder());
}
public Meecrowave(final Builder builder) {
this.configuration = builder;
this.ownedTempDir = new File(configuration.tempDir, "meecrowave_" + System.nanoTime());
}
public Builder getConfiguration() {
return configuration;
}
public File getBase() {
return base;
}
public Tomcat getTomcat() {
return tomcat;
}
public boolean isServing() {
return tomcat != null && tomcat.getHost().getState() == LifecycleState.STARTED;
}
public void undeploy(final String root) {
ofNullable(this.contexts.remove(root)).ifPresent(Runnable::run);
}
public Meecrowave deployClasspath(final DeploymentMeta meta) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
final ClassLoader parentLoader = tomcat.getServer().getParentClassLoader();
if (parentLoader.getParent() == classLoader) {
classLoader = parentLoader;
}
final ProvidedLoader loader = new ProvidedLoader(classLoader, configuration.isTomcatWrapLoader());
final Consumer<Context> builtInCustomizer = c -> c.setLoader(loader);
return deployWebapp(new DeploymentMeta(meta.context, meta.docBase, ofNullable(meta.consumer).map(c -> (Consumer<Context>) ctx -> {
builtInCustomizer.accept(ctx);
c.accept(ctx);
}).orElse(builtInCustomizer)));
}
// shortcut
public Meecrowave deployClasspath() {
return deployClasspath("");
}
// shortcut
public Meecrowave bake(final Consumer<Context> customizer) {
start();
return deployClasspath(new DeploymentMeta("", null, customizer));
}
// shortcut (used by plugins)
public Meecrowave deployClasspath(final String context) {
return deployClasspath(new DeploymentMeta(context, null, null));
}
// shortcut
public Meecrowave deployWebapp(final File warOrDir) {
return deployWebapp("", warOrDir);
}
// shortcut (used by plugins)
public Meecrowave deployWebapp(final String context, final File warOrDir) {
return deployWebapp(new DeploymentMeta(context, warOrDir, null));
}
public Meecrowave deployWebapp(final DeploymentMeta meta) {
if (contexts.containsKey(meta.context)) {
throw new IllegalArgumentException("Already deployed: '" + meta.context + "'");
}
// always nice to see the deployment with something else than internals
final String base = tomcat.getService().findConnectors().length > 0 ?
(configuration.getActiveProtocol() + "://" + tomcat.getHost().getName() + ':' + configuration.getActivePort()) : "";
new LogFacade(Meecrowave.class.getName()).info("--------------- " + base + meta.context);
final OWBJarScanner scanner = new OWBJarScanner();
final StandardContext ctx = new StandardContext() {
@Override
public void setApplicationEventListeners(final Object[] listeners) {
if (listeners == null) {
super.setApplicationEventListeners(null);
return;
}
// ensure owb is first and cxf is last otherwise surprises,
// if we don't -> no @RequestScoped in request listeners :(
for (int i = 1; i < listeners.length; i++) {
if (OWBAutoSetup.EagerBootListener.class.isInstance(listeners[i])) {
final Object first = listeners[0];
listeners[0] = listeners[i];
listeners[i] = first;
break;
}
}
// and finally let it go after our re-ordering
super.setApplicationEventListeners(listeners);
}
};
ctx.setPath(meta.context);
ctx.setName(meta.context);
ctx.setJarScanner(scanner);
ctx.setInstanceManager(new CDIInstanceManager());
ofNullable(meta.docBase).ifPresent(d -> {
try {
ctx.setDocBase(meta.docBase.getCanonicalPath());
} catch (final IOException e) {
ctx.setDocBase(meta.docBase.getAbsolutePath());
}
});
ofNullable(configuration.tomcatFilter).ifPresent(filter -> {
try {
scanner.setJarScanFilter(JarScanFilter.class.cast(Thread.currentThread().getContextClassLoader().loadClass(filter).newInstance()));
} catch (final ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
});
final AtomicReference<Runnable> releaseSCI = new AtomicReference<>();
final ServletContainerInitializer meecrowaveInitializer = (c, ctx1) -> {
new OWBAutoSetup().onStartup(c, ctx1);
if (Cxfs.IS_PRESENT) {
new CxfCdiAutoSetup().onStartup(c, ctx1);
}
new TomcatAutoInitializer().onStartup(c, ctx1);
if (configuration.isInjectServletContainerInitializer()) {
final Field f;
try { // now cdi is on, we can inject cdi beans in ServletContainerInitializer
f = StandardContext.class.getDeclaredField("initializers");
if (!f.isAccessible()) {
f.setAccessible(true);
}
} catch (final Exception e) {
throw new IllegalStateException("Bad tomcat version", e);
}
final List<AutoCloseable> cc;
try {
cc = ((Map<ServletContainerInitializer, Set<Class<?>>>) f.get(ctx)).keySet().stream()
.filter(i -> !i.getClass().getName().startsWith(Meecrowave.class.getName()))
.map(i -> {
try {
return this.inject(i);
} catch (final IllegalArgumentException iae) {
return null;
}
})
.filter(Objects::nonNull)
.collect(toList());
} catch (final IllegalAccessException e) {
throw new IllegalStateException("Can't read initializers", e);
}
releaseSCI.set(() -> cc.forEach(closeable -> {
try {
closeable.close();
} catch (final Exception e) {
throw new IllegalStateException(e);
}
}));
}
};
ctx.addLifecycleListener(new MeecrowaveContextConfig(configuration, meta.docBase != null, meecrowaveInitializer));
ctx.addLifecycleListener(event -> {
switch (event.getType()) {
case Lifecycle.BEFORE_START_EVENT:
if (configuration.getWebSessionCookieConfig() != null) {
final Properties p = new Properties();
try {
p.load(new StringReader(configuration.getWebSessionCookieConfig()));
} catch (final IOException e) {
throw new IllegalArgumentException(e);
}
if (p.containsKey("domain")) {
ctx.setSessionCookieDomain(p.getProperty("domain"));
}
if (p.containsKey("path")) {
ctx.setSessionCookiePath(p.getProperty("path"));
}
if (p.containsKey("name")) {
ctx.setSessionCookieName(p.getProperty("name"));
}
if (p.containsKey("use-trailing-slash")) {
ctx.setSessionCookiePathUsesTrailingSlash(Boolean.parseBoolean(p.getProperty("use-trailing-slash")));
}
if (p.containsKey("http-only")) {
ctx.setUseHttpOnly(Boolean.parseBoolean(p.getProperty("http-only")));
}
if (p.containsKey("secured")) {
final SessionCookieConfig sessionCookieConfig = ctx.getServletContext().getSessionCookieConfig();
sessionCookieConfig.setSecure(Boolean.parseBoolean(p.getProperty("secured")));
}
}
break;
case Lifecycle.AFTER_START_EVENT:
ctx.getResources().setCachingAllowed(configuration.webResourceCached);
break;
case Lifecycle.BEFORE_INIT_EVENT:
ctx.getServletContext().setAttribute("meecrowave.configuration", configuration);
ctx.getServletContext().setAttribute("meecrowave.instance", Meecrowave.this);
if (configuration.loginConfig != null) {
ctx.setLoginConfig(configuration.loginConfig.build());
}
for (final SecurityConstaintBuilder sc : configuration.securityConstraints) {
ctx.addConstraint(sc.build());
}
if (configuration.webXml != null) {
ctx.getServletContext().setAttribute(Globals.ALT_DD_ATTR, configuration.webXml);
}
break;
default:
}
});
ctx.addLifecycleListener(new Tomcat.FixContextListener()); // after having configured the security!!!
ctx.addServletContainerInitializer(meecrowaveInitializer, emptySet());
if (configuration.isUseTomcatDefaults()) {
ctx.setSessionTimeout(configuration.getWebSessionTimeout() != null ? configuration.getWebSessionTimeout() : 30);
ctx.addWelcomeFile("index.html");
ctx.addWelcomeFile("index.htm");
try {
final Field mimesField = Tomcat.class.getDeclaredField("DEFAULT_MIME_MAPPINGS");
if (!mimesField.isAccessible()) {
mimesField.setAccessible(true);
}
final String[] defaultMimes = String[].class.cast(mimesField.get(null));
for (int i = 0; i < defaultMimes.length; ) {
ctx.addMimeMapping(defaultMimes[i++], defaultMimes[i++]);
}
} catch (final NoSuchFieldException | IllegalAccessException e) {
throw new IllegalStateException("Incompatible Tomcat", e);
}
} else if (configuration.getWebSessionTimeout() != null) {
ctx.setSessionTimeout(configuration.getWebSessionTimeout());
}
ofNullable(meta.consumer).ifPresent(c -> c.accept(ctx));
if (configuration.antiResourceLocking && StandardContext.class.isInstance(ctx)) {
StandardContext.class.cast(ctx).setAntiResourceLocking(true);
}
configuration.getInitializers().forEach(i -> ctx.addServletContainerInitializer(i, emptySet()));
configuration.getGlobalContextConfigurers().forEach(it -> it.accept(ctx));
final Host host = tomcat.getHost();
host.addChild(ctx);
final ClassLoader classLoader = ctx.getLoader().getClassLoader();
if (host.getState().isAvailable()) {
fire(new StartListening(findFirstConnector(), host, ctx), classLoader);
}
contexts.put(meta.context, () -> {
if (host.getState().isAvailable()) {
fire(new StopListening(findFirstConnector(), host, ctx), classLoader);
}
ofNullable(releaseSCI.get()).ifPresent(Runnable::run);
tomcat.getHost().removeChild(ctx);
});
return this;
}
public Meecrowave bake() {
return bake("");
}
public Meecrowave bake(final String ctx) {
start();
return deployClasspath(ctx);
}
public Meecrowave start() {
final Map<String, String> systemPropsToRestore = new HashMap<>();
if (configuration.getMeecrowaveProperties() != null && !"meecrowave.properties".equals(configuration.getMeecrowaveProperties())) {
configuration.loadFrom(configuration.getMeecrowaveProperties());
}
if (configuration.isUseLog4j2JulLogManager() && Log4j2s.IS_PRESENT) { // /!\ don't move this line or add anything before without checking log setup
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
if (configuration.loggingGlobalSetup && Log4j2s.IS_PRESENT) {
setSystemProperty(systemPropsToRestore, "log4j.shutdownHookEnabled", "false");
setSystemProperty(systemPropsToRestore, "openwebbeans.logging.factory", Log4j2LoggerFactory.class.getName());
setSystemProperty(systemPropsToRestore, "org.apache.cxf.Logger", Log4j2Logger.class.getName());
setSystemProperty(systemPropsToRestore, "org.apache.tomcat.Logger", Log4j2Log.class.getName());
postTask = () -> {
if (Log4j2s.IS_PRESENT) {
new Log4j2Shutdown().shutdown();
}
systemPropsToRestore.forEach((key, value) -> {
if (value == null) {
System.clearProperty(key);
} else {
System.setProperty(key, value);
}
});
};
}
setupJmx(configuration.isTomcatNoJmx());
clearCatalinaSystemProperties = System.getProperty("catalina.base") == null && System.getProperty("catalina.home") == null;
if (configuration.quickSession) {
tomcat = new TomcatWithFastSessionIDs();
} else {
tomcat = new InternalTomcat();
}
{ // setup
base = new File(newBaseDir());
// create the temp dir folder.
File tempDir;
if (configuration.getTempDir() == null || configuration.getTempDir().length() == 0) {
tempDir = createDirectory(base, "temp");
} else {
tempDir = new File(configuration.getTempDir());
if (!tempDir.exists()) {
tempDir.mkdirs();
}
}
try {
workDir = createDirectory(base, "work");
} catch (final IllegalStateException ise) {
// in case we could not create that directory we create it in the temp dir folder
workDir = createDirectory(tempDir, "work");
}
synchronize(new File(base, "conf"), configuration.conf);
}
final Properties props = configuration.properties;
Substitutor substitutor = null;
for (final String s : props.stringPropertyNames()) {
final String v = props.getProperty(s);
if (v != null && v.contains("${")) {
if (substitutor == null) {
final Map<String, String> placeHolders = new HashMap<>();
placeHolders.put("meecrowave.embedded.http", Integer.toString(configuration.httpPort));
placeHolders.put("meecrowave.embedded.https", Integer.toString(configuration.httpsPort));
placeHolders.put("meecrowave.embedded.stop", Integer.toString(configuration.stopPort));
substitutor = new Substitutor(placeHolders);
}
props.put(s, substitutor.replace(v));
}
}
final File conf = new File(base, "conf");
tomcat.setBaseDir(base.getAbsolutePath());
tomcat.setHostname(configuration.host);
final boolean initialized;
if (configuration.serverXml != null) {
final File file = new File(conf, "server.xml");
if (!file.equals(configuration.serverXml)) {
try (final InputStream is = new FileInputStream(configuration.serverXml);
final FileOutputStream fos = new FileOutputStream(file)) {
IO.copy(is, fos);
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
// respect config (host/port) of the Configuration
final QuickServerXmlParser ports = QuickServerXmlParser.parse(file);
if (configuration.keepServerXmlAsThis) {
configuration.httpPort = Integer.parseInt(ports.http());
configuration.stopPort = Integer.parseInt(ports.stop());
} else {
final Map<String, String> replacements = new HashMap<>();
replacements.put(ports.http(), String.valueOf(configuration.httpPort));
replacements.put(ports.https(), String.valueOf(configuration.httpsPort));
replacements.put(ports.stop(), String.valueOf(configuration.stopPort));
String serverXmlContent;
try (final InputStream stream = new FileInputStream(file)) {
serverXmlContent = IO.toString(stream);
for (final Map.Entry<String, String> pair : replacements.entrySet()) {
serverXmlContent = serverXmlContent.replace(pair.getKey(), pair.getValue());
}
} catch (final IOException e) {
throw new IllegalStateException(e);
}
try (final OutputStream os = new FileOutputStream(file)) {
os.write(serverXmlContent.getBytes(StandardCharsets.UTF_8));
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
tomcat.server(createServer(file.getAbsolutePath()));
initialized = true;
} else {
tomcat.getServer().setPort(configuration.stopPort);
initialized = false;
}
ofNullable(configuration.getSharedLibraries()).map(File::new).filter(File::isDirectory).ifPresent(libRoot -> {
final Collection<URL> libs = new ArrayList<>();
try {
libs.add(libRoot.toURI().toURL());
} catch (final MalformedURLException e) {
throw new IllegalStateException(e);
}
libs.addAll(ofNullable(libRoot.listFiles((dir, name) -> name.endsWith(".jar") || name.endsWith(".zip")))
.map(Stream::of).map(s -> s.map(f -> {
try {
return f.toURI().toURL();
} catch (final MalformedURLException e) {
throw new IllegalStateException(e);
}
}).collect(toList()))
.orElse(emptyList()));
tomcat.getServer().setParentClassLoader(new MeecrowaveContainerLoader(libs.toArray(new URL[libs.size()]), Thread.currentThread().getContextClassLoader()));
});
if (!initialized) {
tomcat.setHostname(configuration.host);
tomcat.getEngine().setDefaultHost(configuration.host);
final StandardHost host = new StandardHost();
host.setName(configuration.host);
try {
final File webapps = createDirectory(base, "webapps");
host.setAppBase(webapps.getAbsolutePath());
} catch (final IllegalStateException ise) {
// never an issue since the webapps are deployed being put in webapps - so no dynamic folder
// or through their path - so don't need webapps folder
}
host.setUnpackWARs(true); // forced for now cause OWB doesn't support war:file:// urls
try {
host.setWorkDir(workDir.getCanonicalPath());
} catch (final IOException e) {
host.setWorkDir(workDir.getAbsolutePath());
}
tomcat.setHost(host);
}
ofNullable(configuration.getTomcatAccessLogPattern())
.ifPresent(pattern -> tomcat.getHost().getPipeline().addValve(new LoggingAccessLogPattern(pattern)));
final List<Valve> valves = buildValves();
if (!valves.isEmpty()) {
final Pipeline pipeline = tomcat.getHost().getPipeline();
valves.forEach(pipeline::addValve);
}
if (configuration.realm != null) {
tomcat.getEngine().setRealm(configuration.realm);
}
if (tomcat.getRawConnector() == null && !configuration.skipHttp) {
final Connector connector = createConnector();
connector.setPort(configuration.httpPort);
if (connector.getAttribute("connectionTimeout") == null) {
connector.setAttribute("connectionTimeout", "3000");
}
tomcat.getService().addConnector(connector);
tomcat.setConnector(connector);
}
// create https connector
if (configuration.ssl) {
final Connector httpsConnector = createConnector();
httpsConnector.setPort(configuration.httpsPort);
httpsConnector.setSecure(true);
httpsConnector.setScheme("https");
httpsConnector.setProperty("SSLEnabled", "true");
if (configuration.sslProtocol != null) {
configuration.property("connector.sslhostconfig.sslProtocol", configuration.sslProtocol);
}
if (configuration.properties.getProperty("connector.sslhostconfig.hostName") != null) {
httpsConnector.setAttribute("defaultSSLHostConfigName", configuration.properties.getProperty("connector.sslhostconfig.hostName"));
}
if (configuration.keystoreFile != null) {
configuration.property("connector.sslhostconfig.certificateKeystoreFile", configuration.keystoreFile);
}
if (configuration.keystorePass != null) {
configuration.property("connector.sslhostconfig.certificateKeystorePassword", configuration.keystorePass);
}
configuration.property("connector.sslhostconfig.certificateKeystoreType", configuration.keystoreType);
if (configuration.clientAuth != null) {
httpsConnector.setAttribute("clientAuth", configuration.clientAuth);
}
if (configuration.keyAlias != null) {
configuration.property("connector.sslhostconfig.certificateKeyAlias", configuration.keyAlias);
}
if (configuration.http2) {
httpsConnector.addUpgradeProtocol(new Http2Protocol());
}
final List<SSLHostConfig> buildSslHostConfig = buildSslHostConfig();
if (!buildSslHostConfig.isEmpty()) {
createDirectory(base, "conf");
}
buildSslHostConfig.forEach(sslHostConf -> {
if (isCertificateFromClasspath(sslHostConf.getCertificateKeystoreFile())) {
copyCertificateToConfDir(sslHostConf.getCertificateKeystoreFile());
sslHostConf.setCertificateKeystoreFile(base.getAbsolutePath() + "/conf/" + sslHostConf.getCertificateKeystoreFile());
}
if (isCertificateFromClasspath(sslHostConf.getCertificateKeyFile())) {
copyCertificateToConfDir(sslHostConf.getCertificateKeyFile());
sslHostConf.setCertificateKeyFile(base.getAbsolutePath() + "/conf/" + sslHostConf.getCertificateKeyFile());
copyCertificateToConfDir(sslHostConf.getCertificateFile());
sslHostConf.setCertificateFile(base.getAbsolutePath() + "/conf/" + sslHostConf.getCertificateFile());
}
if (isCertificateFromClasspath(sslHostConf.getTruststoreFile())) {
copyCertificateToConfDir(sslHostConf.getTruststoreFile());
sslHostConf.setTruststoreFile(base.getAbsolutePath() + "/conf/" + sslHostConf.getTruststoreFile());
}
if (isCertificateFromClasspath(sslHostConf.getCertificateChainFile())) {
copyCertificateToConfDir(sslHostConf.getCertificateChainFile());
sslHostConf.setCertificateChainFile(base.getAbsolutePath() + "/conf/" + sslHostConf.getCertificateChainFile());
}
});
buildSslHostConfig.forEach(httpsConnector::addSslHostConfig);
if (configuration.defaultSSLHostConfigName != null) {
httpsConnector.setAttribute("defaultSSLHostConfigName", configuration.defaultSSLHostConfigName);
}
tomcat.getService().addConnector(httpsConnector);
if (configuration.skipHttp) {
tomcat.setConnector(httpsConnector);
}
}
for (final Connector c : configuration.connectors) {
tomcat.getService().addConnector(c);
}
if (!configuration.skipHttp && !configuration.ssl && !configuration.connectors.isEmpty()) {
tomcat.setConnector(configuration.connectors.iterator().next());
}
if (configuration.users != null) {
for (final Map.Entry<String, String> user : configuration.users.entrySet()) {
tomcat.addUser(user.getKey(), user.getValue());
}
}
if (configuration.roles != null) {
for (final Map.Entry<String, String> user : configuration.roles.entrySet()) {
for (final String role : user.getValue().split(" *, *")) {
tomcat.addRole(user.getKey(), role);
}
}
}
StreamSupport.stream(ServiceLoader.load(Meecrowave.InstanceCustomizer.class).spliterator(), false)
.peek(i -> {
if (MeecrowaveAwareInstanceCustomizer.class.isInstance(i)) {
MeecrowaveAwareInstanceCustomizer.class.cast(i).setMeecrowave(this);
}
})
.forEach(c -> c.accept(tomcat));
configuration.instanceCustomizers.forEach(c -> c.accept(tomcat));
StreamSupport.stream(ServiceLoader.load(Meecrowave.ContextCustomizer.class).spliterator(), false)
.peek(i -> {
if (MeecrowaveAwareContextCustomizer.class.isInstance(i)) {
MeecrowaveAwareContextCustomizer.class.cast(i).setMeecrowave(this);
}
})
.forEach(configuration::addGlobalContextCustomizer);
beforeStart();
if (configuration.initializeClientBus && Cxfs.IS_PRESENT && !Cxfs.hasDefaultBus()) {
clientBus = new ConfigurableBus();
clientBus.initProviders(configuration,
ofNullable(Thread.currentThread().getContextClassLoader()).orElseGet(ClassLoader::getSystemClassLoader));
clientBus.addClientLifecycleListener();
}
try {
if (!initialized) {
tomcat.init();
}
tomcat.getHost().addLifecycleListener(event -> {
if (!Host.class.isInstance(event.getSource())) {
return;
}
broadcastHostEvent(event.getType(), Host.class.cast(event.getSource()));
});
tomcat.start();
} catch (final LifecycleException e) {
throw new IllegalStateException(e);
}
ofNullable(configuration.getPidFile()).ifPresent(pidFile -> {
if (pidFile.getParentFile() != null && !pidFile.getParentFile().isDirectory() && !pidFile.getParentFile().mkdirs()) {
throw new IllegalArgumentException("Can't create " + pidFile);
}
final String pid = ManagementFactory.getRuntimeMXBean().getName();
final int at = pid.indexOf('@');
try (final Writer w = new FileWriter(pidFile)) {
w.write(at > 0 ? pid.substring(0, at) : pid);
} catch (final IOException e) {
throw new IllegalStateException("Can't write the pid in " + pid, e);
}
});
if (configuration.isUseShutdownHook()) {
hook = new Thread(() -> {
hook = null; // prevent close to remove the hook which would throw an exception
close();
}, "meecrowave-stop-hook");
Runtime.getRuntime().addShutdownHook(hook);
}
return this;
}
private boolean isCertificateFromClasspath(final String certificate) {
final BiPredicate<String, String> equals = System.getProperty("os.name", "ignore").toLowerCase(ROOT).contains("win") ?
String::equalsIgnoreCase : String::equals;
return certificate != null && !(new File(certificate).exists())
&& !equals.test(
Paths.get(System.getProperty("user.home")).resolve(".keystore").toAbsolutePath().normalize().toString(),
Paths.get(certificate).toAbsolutePath().normalize().toString());
}
private void copyCertificateToConfDir(String certificate) {
InputStream resourceAsStream = null;
try {
final Path dstFile = Paths.get(base.getAbsolutePath() + "/conf/" + certificate);
resourceAsStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(certificate);
if (resourceAsStream == null) {
resourceAsStream = new FileInputStream(new File((this.getClass().getResource("/").toString().replaceAll("file:", "") + "/" + certificate)));
}
Files.copy(resourceAsStream, dstFile, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new IllegalStateException(e);
} finally {
if (resourceAsStream != null) {
try {
resourceAsStream.close();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
}
/**
* Store away the current system property for restoring it later
* during shutdown.
* @param backupPropertyMap a Map to store away the previous value before setting the newValue
* @param propertyKey
* @param newValue
*/
private void setSystemProperty(Map<String, String> backupPropertyMap, String propertyKey, String newValue) {
backupPropertyMap.put(propertyKey, System.getProperty(propertyKey));
System.setProperty(propertyKey, newValue);
}
private void broadcastHostEvent(final String event, final Host host) {
switch (event) {
case Lifecycle.AFTER_START_EVENT: {
final Connector connector = findFirstConnector();
findContexts(host).forEach(ctx -> fire(new StartListening(connector, host, ctx), ctx.getLoader().getClassLoader()));
break;
}
case Lifecycle.BEFORE_STOP_EVENT: {
final Connector connector = findFirstConnector();
findContexts(host).forEach(ctx -> fire(new StopListening(connector, host, ctx), ctx.getLoader().getClassLoader()));
break;
}
default:
}
}
private Connector findFirstConnector() {
return Stream.of(tomcat.getServer().findServices())
.flatMap(s -> Stream.of(s.findConnectors()))
.findFirst()
.orElse(null);
}
private Stream<Context> findContexts(final Host host) {
return Stream.of(host.findChildren())
.filter(Context.class::isInstance)
.map(Context.class::cast)
.filter(ctx -> ctx.getState().isAvailable());
}
private <T> void fire(final T event, final ClassLoader classLoader) {
final Thread thread = Thread.currentThread();
final ClassLoader loader = thread.getContextClassLoader();
thread.setContextClassLoader(classLoader);
try {
WebBeansContext.currentInstance()
.getBeanManagerImpl()
.getEvent()
.select(Class.class.cast(event.getClass()))
.fire(event);
} finally {
thread.setContextClassLoader(loader);
}
}
private void setupJmx(final boolean skip) {
if (skip) {
Registry.disableRegistry();
}
}
/**
* Syntax uses:
* <code>
* valves.myValve1._className = org.apache.meecrowave.tomcat.LoggingAccessLogPattern
* valves.myValve1._order = 0
*
* valves.myValve1._className = SSOVa
* valves.myValve1._order = 1
* valves.myValve1.showReportInfo = false
* </code>
*
* @return the list of valve from the properties.
*/
private List<Valve> buildValves() {
final List<Valve> valves = new ArrayList<>();
configuration.properties.stringPropertyNames().stream()
.filter(key -> key.startsWith("valves.") && key.endsWith("._className"))
.sorted(comparing(key -> Integer.parseInt(configuration.properties
.getProperty(key.replaceFirst("\\._className$", "._order"), "0"))))
.map(key -> key.split("\\."))
.filter(parts -> parts.length == 3)
.forEach(key -> {
final String prefix = key[0] + '.' + key[1] + '.';
final ObjectRecipe recipe = newRecipe(configuration.properties.getProperty(prefix + key[2]));
configuration.properties.stringPropertyNames().stream()
.filter(it -> it.startsWith(prefix) && !it.endsWith("._order") && !it.endsWith("._className"))
.forEach(propKey -> {
final String value = configuration.properties.getProperty(propKey);
recipe.setProperty(propKey.substring(prefix.length()), value);
});
valves.add(Valve.class.cast(recipe.create(Thread.currentThread().getContextClassLoader())));
});
return valves;
}
private List<SSLHostConfig> buildSslHostConfig() {
final List<SSLHostConfig> sslHostConfigs = new ArrayList<>();
// Configures default SSLHostConfig
final ObjectRecipe defaultSslHostConfig = newRecipe(SSLHostConfig.class.getName());
for (final String key : configuration.properties.stringPropertyNames()) {
if (key.startsWith("connector.sslhostconfig.") && key.split("\\.").length == 3) {
final String substring = key.substring("connector.sslhostconfig.".length());
defaultSslHostConfig.setProperty(substring, configuration.properties.getProperty(key));
}
}
if (!defaultSslHostConfig.getProperties().isEmpty()) {
sslHostConfigs.add(SSLHostConfig.class.cast(defaultSslHostConfig.create()));
}
// Allows to add N Multiple SSLHostConfig elements not including the default one.
final Collection<Integer> itemNumbers = configuration.properties.stringPropertyNames()
.stream()
.filter(key -> (key.startsWith("connector.sslhostconfig.") && key.split("\\.").length == 4))
.map(key -> Integer.parseInt(key.split("\\.")[2]))
.collect(toSet());
itemNumbers.stream().sorted().forEach(itemNumber -> {
final ObjectRecipe recipe = newRecipe(SSLHostConfig.class.getName());
final String prefix = "connector.sslhostconfig." + itemNumber + '.';
configuration.properties.stringPropertyNames().stream()
.filter(k -> k.startsWith(prefix))
.forEach(key -> {
final String keyName = key.split("\\.")[3];
recipe.setProperty(keyName, configuration.properties.getProperty(key));
});
if (!recipe.getProperties().isEmpty()) {
final SSLHostConfig sslHostConfig = SSLHostConfig.class.cast(recipe.create());
sslHostConfigs.add(sslHostConfig);
new LogFacade(Meecrowave.class.getName())
.info("Created SSLHostConfig #" + itemNumber + " (" + sslHostConfig.getHostName() + ")");
}
});
return sslHostConfigs;
}
protected void beforeStart() {
// no-op
}
protected void beforeStop() {
// no-op
}
public <T> AutoCloseable inject(final T instance) {
final BeanManager bm = CDI.current().getBeanManager();
final AnnotatedType<?> annotatedType = bm.createAnnotatedType(instance.getClass());
final InjectionTarget injectionTarget = bm.createInjectionTarget(annotatedType);
final CreationalContext<Object> creationalContext = bm.createCreationalContext(null);
injectionTarget.inject(instance, creationalContext);
return creationalContext::release;
}
@Override
public synchronized void close() {
if (tomcat == null) {
return;
}
if (hook != null) {
Runtime.getRuntime().removeShutdownHook(hook);
this.hook = null;
}
beforeStop();
if (MeecrowaveContainerLoader.class.isInstance(tomcat.getServer().getParentClassLoader())) {
try {
MeecrowaveContainerLoader.class.cast(tomcat.getServer().getParentClassLoader()).close();
} catch (final IOException e) {
new LogFacade(Meecrowave.class.getName()).error(e.getMessage(), e);
}
}
try {
contexts.values().forEach(Runnable::run);
} finally {
try {
tomcat.stop();
tomcat.destroy();
} catch (final LifecycleException e) {
throw new IllegalStateException(e);
} finally {
Cxfs.resetDefaultBusIfEquals(clientBus); // after if runnables or listeners trigger CXF
tomcat = null; // ensure we can call close() N times and not have side effects
contexts.clear();
if (clearCatalinaSystemProperties) {
Stream.of("catalina.base", "catalina.home").forEach(System::clearProperty);
}
if (configuration.isUseLog4j2JulLogManager()) {
System.clearProperty("java.util.logging.manager");
}
ofNullable(postTask).ifPresent(Runnable::run);
postTask = null;
try {
if (deleteBase && base != null) {
IO.delete(base);
}
if (ownedTempDir != null) {
IO.delete(ownedTempDir);
}
} catch (final IllegalArgumentException /*does not exist from the hook*/ e) {
// no-op
} finally {
base = null;
// not very important if we can't delete it since next restart will write another value normally
ofNullable(configuration.getPidFile()).ifPresent(File::delete);
}
}
}
}
protected Connector createConnector() {
final Connector connector;
final Properties properties = configuration.properties;
if (properties != null) {
final Map<String, String> attributes = new HashMap<>();
final ObjectRecipe recipe = newRecipe(Connector.class.getName());
for (final String key : properties.stringPropertyNames()) {
if (!key.startsWith("connector.")) {
continue;
}
final String substring = key.substring("connector.".length());
if (substring.startsWith("sslhostconfig.")) {
continue;
}
if (!substring.startsWith("attributes.")) {
recipe.setProperty(substring, properties.getProperty(key));
} else {
attributes.put(substring.substring("attributes.".length()), properties.getProperty(key));
}
}
connector = recipe.getProperties().isEmpty() ? new Connector() : Connector.class.cast(recipe.create());
for (final Map.Entry<String, String> attr : attributes.entrySet()) {
connector.setAttribute(attr.getKey(), attr.getValue());
}
} else {
connector = new Connector();
}
return connector;
}
private static Server createServer(final String serverXml) {
final Catalina catalina = new Catalina() {
// skip few init we don't need *here*
@Override
protected void initDirs() {
// no-op
}
@Override
protected void initStreams() {
// no-op
}
@Override
protected void initNaming() {
// no-op
}
};
catalina.setConfigFile(serverXml);
catalina.load();
return catalina.getServer();
}
private File createDirectory(final File parent, final String directory) {
final File dir = new File(parent, directory);
IO.mkdirs(dir);
return dir;
}
private void synchronize(final File base, final String resourceBase) {
if (resourceBase == null) {
return;
}
try {
final Map<String, URL> urls = new ResourceFinder("").getResourcesMap(resourceBase);
for (final Map.Entry<String, URL> u : urls.entrySet()) {
try (final InputStream is = u.getValue().openStream()) {
final File to = new File(base, u.getKey());
final File parentFile = to.getParentFile();
createDirectory(parentFile.getParentFile(), parentFile.getName());
try (final OutputStream os = new FileOutputStream(to)) {
IO.copy(is, os);
}
if ("server.xml".equals(u.getKey())) {
configuration.setServerXml(to.getAbsolutePath());
}
}
}
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
private String newBaseDir() {
deleteBase = false;
String dir = configuration.dir;
if (dir != null) {
final File dirFile = new File(dir);
if (dirFile.exists()) {
if (base != null && base.exists() && configuration.deleteBaseOnStartup) {
IO.delete(base);
}
return dir;
}
IO.mkdirs(dirFile);
return dirFile.getAbsolutePath();
}
final String base = System.getProperty("meecrowave.base");
if (base != null && new File(base).exists()) {
return new File(base).getAbsolutePath();
}
deleteBase = true;
final List<String> lookupPaths = new ArrayList<>();
lookupPaths.add("target");
lookupPaths.add("build");
final File file = lookupPaths.stream()
.map(File::new)
.filter(File::isDirectory)
.findFirst()
.map(file1 -> new File(file1, "meecrowave-" + System.nanoTime())).orElse(ownedTempDir);
IO.mkdirs(file);
return file.getAbsolutePath();
}
public void await() {
tomcat.getServer().await();
}
private static ObjectRecipe newRecipe(final String clazz) {
final ObjectRecipe recipe = new ObjectRecipe(clazz);
recipe.allow(Option.FIELD_INJECTION);
recipe.allow(Option.PRIVATE_PROPERTIES);
return recipe;
}
// this class holds all the built-in config,
// extension can use extensions feature (see cli.html) which is basically the same kind of bean
// accessible through builder.getExtension(type) builder being accessible through the meecrowave.configuration
// attribute of the ServletContext.
public static class Builder {
@CliOption(name = "pid-file", description = "A file path to write the process id if the server starts")
private File pidFile;
@CliOption(name = "watcher-bouncing", description = "Activate redeployment on directories update using this bouncing.")
private int watcherBouncing = 500;
@CliOption(name = "http", description = "HTTP port")
private int httpPort = 8080;
@CliOption(name = "https", description = "HTTPS port")
private int httpsPort = 8443;
@CliOption(name = "stop", description = "Shutdown port if used or -1")
private int stopPort = -1;
@CliOption(name = "host", description = "Default host")
private String host = "localhost";
@CliOption(name = "dir", description = "Root folder if provided otherwise a fake one is created in tmp-dir")
private String dir;
@CliOption(name = "server-xml", description = "Provided server.xml")
private File serverXml;
@CliOption(name = "keep-server-xml-as-this", description = "Don't replace ports in server.xml")
private boolean keepServerXmlAsThis;
@CliOption(name = "properties", description = "Passthrough properties")
private Properties properties = new Properties();
@CliOption(name = "quick-session", description = "Should an unsecured but fast session id generator be used")
private boolean quickSession = true;
@CliOption(name = "skip-http", description = "Skip HTTP connector")
private boolean skipHttp;
@CliOption(name = "ssl", description = "Use HTTPS")
private boolean ssl;
@CliOption(name = "keystore-file", description = "HTTPS keystore location")
private String keystoreFile;
@CliOption(name = "keystore-password", description = "HTTPS keystore password")
private String keystorePass;
@CliOption(name = "keystore-type", description = "HTTPS keystore type")
private String keystoreType = "JKS";
@CliOption(name = "client-auth", description = "HTTPS keystore client authentication")
private String clientAuth;
@CliOption(name = "keystore-alias", description = "HTTPS keystore alias")
private String keyAlias;
@CliOption(name = "ssl-protocol", description = "HTTPS protocol")
private String sslProtocol;
@CliOption(name = "web-xml", description = "Global web.xml")
private String webXml;
@CliOption(name = "login-config", description = "web.xml login config")
private LoginConfigBuilder loginConfig;
@CliOption(name = "security-constraint", description = "web.xml security constraint")
private Collection<SecurityConstaintBuilder> securityConstraints = new LinkedList<>();
@CliOption(name = "realm", description = "realm")
private Realm realm;
@CliOption(name = "users", description = "In memory users")
private Map<String, String> users;
@CliOption(name = "roles", description = "In memory roles")
private Map<String, String> roles;
@CliOption(name = "http2", description = "Activate HTTP 2")
private boolean http2;
@CliOption(name = "connector", description = "Custom connectors")
private final Collection<Connector> connectors = new ArrayList<>();
@CliOption(name = "tmp-dir", description = "Temporary directory")
private String tempDir = System.getProperty("java.io.tmpdir");
@CliOption(name = "web-resource-cached", description = "Cache web resources")
private boolean webResourceCached = true;
@CliOption(name = "conf", description = "Conf folder to synchronize")
private String conf;
@CliOption(name = "delete-on-startup", description = "Should the directory be cleaned on startup if existing")
private boolean deleteBaseOnStartup = true;
@CliOption(name = "jaxrs-mapping", description = "Default jaxrs mapping")
private String jaxrsMapping = "/*";
@CliOption(name = "cdi-conversation", description = "Should CDI conversation be activated")
private boolean cdiConversation;
@CliOption(name = "jaxrs-provider-setup", description = "Should default JAX-RS provider be configured")
private boolean jaxrsProviderSetup = true;
@CliOption(name = "jaxrs-default-providers", description = "If jaxrsProviderSetup is true the list of default providers to load (or defaulting to johnson jsonb and jsonp ones)")
private String jaxrsDefaultProviders;
@CliOption(name = "jaxrs-beanvalidation", description = "Should bean validation be activated on JAX-RS endpoint if present in the classpath.")
private boolean jaxrsAutoActivateBeanValidation = true;
@CliOption(name = "jaxrs-log-provider", description = "Should JAX-RS providers be logged")
private boolean jaxrsLogProviders = false;
@CliOption(name = "jsonp-buffer-strategy", description = "JSON-P JAX-RS provider buffer strategy (see johnzon)")
private String jsonpBufferStrategy = "QUEUE";
@CliOption(name = "jsonp-max-string-length", description = "JSON-P JAX-RS provider max string limit size (see johnzon)")
private int jsonpMaxStringLen = 64 * 1024;
@CliOption(name = "jsonp-read-buffer-length", description = "JSON-P JAX-RS provider read buffer limit size (see johnzon)")
private int jsonpMaxReadBufferLen = 64 * 1024;
@CliOption(name = "jsonp-write-buffer-length", description = "JSON-P JAX-RS provider write buffer limit size (see johnzon)")
private int jsonpMaxWriteBufferLen = 64 * 1024;
@CliOption(name = "jsonp-supports-comment", description = "Should JSON-P JAX-RS provider support comments (see johnzon)")
private boolean jsonpSupportsComment = false;
@CliOption(name = "jsonp-supports-comment", description = "Should JSON-P JAX-RS provider prettify the outputs (see johnzon)")
private boolean jsonpPrettify = false;
@CliOption(name = "jsonb-encoding", description = "Which encoding provider JSON-B should use")
private String jsonbEncoding = "UTF-8";
@CliOption(name = "jsonb-nulls", description = "Should JSON-B provider serialize nulls")
private boolean jsonbNulls = false;
@CliOption(name = "jsonb-ijson", description = "Should JSON-B provider comply to I-JSON")
private boolean jsonbIJson = false;
@CliOption(name = "jsonb-prettify", description = "Should JSON-B provider prettify the output")
private boolean jsonbPrettify = false;
@CliOption(name = "jsonb-binary-strategy", description = "Should JSON-B provider prettify the output")
private String jsonbBinaryStrategy;
@CliOption(name = "jsonb-naming-strategy", description = "Should JSON-B provider prettify the output")
private String jsonbNamingStrategy;
@CliOption(name = "jsonb-order-strategy", description = "Should JSON-B provider prettify the output")
private String jsonbOrderStrategy;
@CliOption(name = "logging-global-setup", description = "Should logging be configured to use log4j2 (it is global)")
private boolean loggingGlobalSetup = true;
@CliOption(name = "cxf-servlet-params", description = "Init parameters passed to CXF servlet")
private Map<String, String> cxfServletParams;
@CliOption(name = "tomcat-scanning", description = "Should Tomcat scanning be used (@HandleTypes, @WebXXX)")
private boolean tomcatScanning = true;
@CliOption(name = "tomcat-default-setup", description = "Add default servlet")
private boolean tomcatAutoSetup = true;
@CliOption(name = "tomcat-default-setup-jsp-development", description = "Should JSP support if available be set in development mode")
private boolean tomcatJspDevelopment = false;
@CliOption(name = "use-shutdown-hook", description = "Use shutdown hook to automatically stop the container on Ctrl+C")
private boolean useShutdownHook = true;
@CliOption(name = "tomcat-filter", description = "A Tomcat JarScanFilter")
private String tomcatFilter;
@CliOption(name = "scanning-include", description = "A forced include list of jar names (comma separated values)")
private String scanningIncludes;
@CliOption(name = "scanning-exclude", description = "A forced exclude list of jar names (comma separated values)")
private String scanningExcludes;
@CliOption(name = "scanning-package-include", description = "A forced include list of packages names (comma separated values)")
private String scanningPackageIncludes;
@CliOption(name = "scanning-package-exclude", description = "A forced exclude list of packages names (comma separated values)")
private String scanningPackageExcludes;
@CliOption(name = "web-session-timeout", description = "Force the session timeout for webapps")
private Integer webSessionTimeout;
@CliOption(name = "web-session-cookie-config", description = "Force the cookie-config, it uses a properties syntax with the keys being the web.xml tag names.")
private String webSessionCookieConfig;
@CliOption(name = "tomcat-default", description = "Should Tomcat default be set (session timeout, mime mapping etc...)")
private boolean useTomcatDefaults = true;
@CliOption(name = "tomcat-wrap-loader", description = "(Experimental) When deploying a classpath (current classloader), " +
"should meecrowave wrap the loader to define another loader identity but still use the same classes and resources.")
private boolean tomcatWrapLoader = false;
@CliOption(name = "tomcat-skip-jmx", description = "(Experimental) Should Tomcat MBeans be skipped.")
private boolean tomcatNoJmx = true;
@CliOption(name = "shared-libraries", description = "A folder containing shared libraries.", alias = "shared-librairies")
private String sharedLibraries;
@CliOption(name = "log4j2-jul-bridge", description = "Should JUL logs be redirected to Log4j2 - only works before JUL usage.")
private boolean useLog4j2JulLogManager = System.getProperty("java.util.logging.manager") == null;
@CliOption(name = "servlet-container-initializer-injection", description = "Should ServletContainerInitialize support injections.")
private boolean injectServletContainerInitializer = true;
@CliOption(
name = "tomcat-access-log-pattern",
description = "Activates and configure the access log valve. Value example: '%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"'")
private String tomcatAccessLogPattern;
@CliOption(
name = "meecrowave-properties",
description = "Loads a meecrowave properties, defaults to meecrowave.properties.")
private String meecrowaveProperties = "meecrowave.properties";
@CliOption(name = "jaxws-support-if-present", description = "Should @WebService CDI beans be deployed if cxf-rt-frontend-jaxws is in the classpath.")
private boolean jaxwsSupportIfAvailable = true;
@CliOption(name = "default-ssl-hostconfig-name", description = "The name of the default SSLHostConfig that will be used for secure https connections.")
private String defaultSSLHostConfigName;
@CliOption(name = "cxf-initialize-client-bus", description = "Should the client bus be set. If false the server one will likely be reused.")
private boolean initializeClientBus = true;
private final Map<Class<?>, Object> extensions = new HashMap<>();
private final Collection<Consumer<Tomcat>> instanceCustomizers = new ArrayList<>();
@CliOption(name = "servlet-container-initializer", description = "ServletContainerInitializer instances.")
private Collection<ServletContainerInitializer> initializers = new ArrayList<>();
@CliOption(name = "tomcat-antiresourcelocking", description = "Should Tomcat anti resource locking feature be activated on StandardContext.")
private boolean antiResourceLocking;
@CliOption(name = "tomcat-context-configurer", description = "Configurers for all webapps. The Consumer<Context> instances will be applied to all deployments.")
private Collection<Consumer<Context>> contextConfigurers;
public Builder() { // load defaults
extensions.put(ValueTransformers.class, new ValueTransformers());
StreamSupport.stream(ServiceLoader.load(Meecrowave.ConfigurationCustomizer.class).spliterator(), false)
.forEach(c -> c.accept(this));
loadFrom(meecrowaveProperties);
}
public <T> T getExtension(final Class<T> extension) {
// in the cli we read the values from the cli but in other mode from properties
// to ensure we can do the same in all modes keeping a nice cli
return extension.cast(extensions.computeIfAbsent(extension, k -> {
try {
return bind(k.newInstance());
} catch (final InstantiationException | IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
}));
}
public boolean isAntiResourceLocking() {
return antiResourceLocking;
}
public void setAntiResourceLocking(final boolean antiResourceLocking) {
this.antiResourceLocking = antiResourceLocking;
}
public Collection<Consumer<Context>> getGlobalContextConfigurers() {
return ofNullable(contextConfigurers).orElseGet(Collections::emptySet);
}
public boolean isTomcatJspDevelopment() {
return tomcatJspDevelopment;
}
public void setTomcatJspDevelopment(final boolean tomcatJspDevelopment) {
this.tomcatJspDevelopment = tomcatJspDevelopment;
}
public Integer getWebSessionTimeout() {
return webSessionTimeout;
}
public void setWebSessionTimeout(final Integer webSessionTimeout) {
this.webSessionTimeout = webSessionTimeout;
}
public String getWebSessionCookieConfig() {
return webSessionCookieConfig;
}
public void setWebSessionCookieConfig(final String webSessionCookieConfig) {
this.webSessionCookieConfig = webSessionCookieConfig;
}
public boolean isInitializeClientBus() {
return initializeClientBus;
}
public void setInitializeClientBus(final boolean initializeClientBus) {
this.initializeClientBus = initializeClientBus;
}
public boolean isJaxwsSupportIfAvailable() {
return jaxwsSupportIfAvailable;
}
public void setJaxwsSupportIfAvailable(final boolean jaxwsSupportIfAvailable) {
this.jaxwsSupportIfAvailable = jaxwsSupportIfAvailable;
}
public int getWatcherBouncing() {
return watcherBouncing;
}
public void setWatcherBouncing(final int watcherBouncing) {
this.watcherBouncing = watcherBouncing;
}
public String getTomcatAccessLogPattern() {
return tomcatAccessLogPattern;
}
public void setTomcatAccessLogPattern(final String tomcatAccessLogPattern) {
this.tomcatAccessLogPattern = tomcatAccessLogPattern;
}
public boolean isTomcatNoJmx() {
return tomcatNoJmx;
}
public void setTomcatNoJmx(final boolean tomcatNoJmx) {
this.tomcatNoJmx = tomcatNoJmx;
}
public File getPidFile() {
return pidFile;
}
public void setPidFile(final File pidFile) {
this.pidFile = pidFile;
}
public String getScanningPackageIncludes() {
return scanningPackageIncludes;
}
/**
* Define some package names (startsWith) which must get scanned for beans.
* This rule get's applied before {@link #setScanningPackageExcludes(String)}
*/
public void setScanningPackageIncludes(final String scanningPackageIncludes) {
this.scanningPackageIncludes = scanningPackageIncludes;
}
public String getScanningPackageExcludes() {
return scanningPackageExcludes;
}
/**
* Define some package names (startsWith) which must <em>NOT</em> get scanned for beans.
* This rule get's applied after {@link #setScanningPackageIncludes(String)}.
*
* Defining just a '*' will be a marker for skipping all not-included packages.
* Otherwise we will defer to the standard OpenWebBeans class Filter mechanism.
*/
public void setScanningPackageExcludes(final String scanningPackageExcludes) {
this.scanningPackageExcludes = scanningPackageExcludes;
}
public Builder excludePackages(final String packages) {
this.setScanningPackageExcludes(packages);
return this;
}
/**
* Only scan the very packages given (startsWith).
* This will exclude <em>all</em> other packages from bean scanning
*/
public Builder includePackages(final String packages) {
this.setScanningPackageIncludes(packages);
this.setScanningPackageExcludes("*");
return this;
}
/**
* Scan the very packages given (startsWith) <em>in addition</em> to the default rules.
*/
public Builder withPackages(final String packages) {
this.setScanningPackageIncludes(packages);
return this;
}
public void setExtension(final Class<?> type, final Object value) {
extensions.put(type, value);
}
public String getScanningIncludes() {
return scanningIncludes;
}
public void setScanningIncludes(final String scanningIncludes) {
this.scanningIncludes = scanningIncludes;
}
public String getScanningExcludes() {
return scanningExcludes;
}
public void setScanningExcludes(final String scanningExcludes) {
this.scanningExcludes = scanningExcludes;
}
public String getJsonpBufferStrategy() {
return jsonpBufferStrategy;
}
public String getJsonbEncoding() {
return jsonbEncoding;
}
public void setJsonbEncoding(final String jsonbEncoding) {
this.jsonbEncoding = jsonbEncoding;
}
public boolean isJsonbNulls() {
return jsonbNulls;
}
public void setJsonbNulls(final boolean jsonbNulls) {
this.jsonbNulls = jsonbNulls;
}
public boolean isJsonbIJson() {
return jsonbIJson;
}
public void setJsonbIJson(final boolean jsonbIJson) {
this.jsonbIJson = jsonbIJson;
}
public boolean isJsonbPrettify() {
return jsonbPrettify;
}
public void setJsonbPrettify(final boolean jsonbPrettify) {
this.jsonbPrettify = jsonbPrettify;
}
public String getJsonbBinaryStrategy() {
return jsonbBinaryStrategy;
}
public void setJsonbBinaryStrategy(final String jsonbBinaryStrategy) {
this.jsonbBinaryStrategy = jsonbBinaryStrategy;
}
public String getJsonbNamingStrategy() {
return jsonbNamingStrategy;
}
public void setJsonbNamingStrategy(final String jsonbNamingStrategy) {
this.jsonbNamingStrategy = jsonbNamingStrategy;
}
public String getJsonbOrderStrategy() {
return jsonbOrderStrategy;
}
public void setJsonbOrderStrategy(final String jsonbOrderStrategy) {
this.jsonbOrderStrategy = jsonbOrderStrategy;
}
public Builder withJsonpBufferStrategy(final String jsonpBufferStrategy) {
this.jsonpBufferStrategy = jsonpBufferStrategy;
return this;
}
public void setJsonpBufferStrategy(final String jsonpBufferStrategy) {
this.jsonpBufferStrategy = jsonpBufferStrategy;
}
public int getJsonpMaxStringLen() {
return jsonpMaxStringLen;
}
public void setJsonpMaxStringLen(final int jsonpMaxStringLen) {
this.jsonpMaxStringLen = jsonpMaxStringLen;
}
public int getJsonpMaxReadBufferLen() {
return jsonpMaxReadBufferLen;
}
public void setJsonpMaxReadBufferLen(final int jsonpMaxReadBufferLen) {
this.jsonpMaxReadBufferLen = jsonpMaxReadBufferLen;
}
public int getJsonpMaxWriteBufferLen() {
return jsonpMaxWriteBufferLen;
}
public void setJsonpMaxWriteBufferLen(final int jsonpMaxWriteBufferLen) {
this.jsonpMaxWriteBufferLen = jsonpMaxWriteBufferLen;
}
public boolean isJsonpSupportsComment() {
return jsonpSupportsComment;
}
public void setJsonpSupportsComment(final boolean jsonpSupportsComment) {
this.jsonpSupportsComment = jsonpSupportsComment;
}
public boolean isJsonpPrettify() {
return jsonpPrettify;
}
public void setJsonpPrettify(final boolean jsonpPrettify) {
this.jsonpPrettify = jsonpPrettify;
}
public String getSharedLibraries() {
return sharedLibraries;
}
public Builder sharedLibraries(final String sharedLibraries) {
setSharedLibraries(sharedLibraries);
return this;
}
public void setSharedLibraries(final String sharedLibraries) {
this.sharedLibraries = sharedLibraries;
}
public boolean isJaxrsLogProviders() {
return jaxrsLogProviders;
}
public void setJaxrsLogProviders(final boolean jaxrsLogProviders) {
this.jaxrsLogProviders = jaxrsLogProviders;
}
public boolean isUseTomcatDefaults() {
return useTomcatDefaults;
}
public void setUseTomcatDefaults(final boolean useTomcatDefaults) {
this.useTomcatDefaults = useTomcatDefaults;
}
public String getTomcatFilter() {
return tomcatFilter;
}
public void setTomcatFilter(final String tomcatFilter) {
this.tomcatFilter = tomcatFilter;
}
public boolean isTomcatScanning() {
return tomcatScanning;
}
public void setTomcatScanning(final boolean tomcatScanning) {
this.tomcatScanning = tomcatScanning;
}
public Map<String, String> getCxfServletParams() {
return cxfServletParams;
}
public void setCxfServletParams(final Map<String, String> cxfServletParams) {
this.cxfServletParams = cxfServletParams;
}
public boolean isLoggingGlobalSetup() {
return loggingGlobalSetup;
}
public void setLoggingGlobalSetup(final boolean loggingGlobalSetup) {
this.loggingGlobalSetup = loggingGlobalSetup;
}
public boolean isJaxrsAutoActivateBeanValidation() {
return jaxrsAutoActivateBeanValidation;
}
public void setJaxrsAutoActivateBeanValidation(final boolean jaxrsAutoActivateBeanValidation) {
this.jaxrsAutoActivateBeanValidation = jaxrsAutoActivateBeanValidation;
}
public boolean isJaxrsProviderSetup() {
return jaxrsProviderSetup;
}
public void setJaxrsProviderSetup(final boolean jaxrsProviderSetup) {
this.jaxrsProviderSetup = jaxrsProviderSetup;
}
public int getHttpPort() {
return httpPort;
}
public void setHttpPort(final int httpPort) {
this.httpPort = httpPort;
}
public int getHttpsPort() {
return httpsPort;
}
public void setHttpsPort(final int httpsPort) {
this.httpsPort = httpsPort;
}
public int getStopPort() {
return stopPort;
}
public void setStopPort(final int stopPort) {
this.stopPort = stopPort;
}
public String getHost() {
return host;
}
public void setHost(final String host) {
this.host = host;
}
public String getDir() {
return dir;
}
public void setDir(final String dir) {
this.dir = dir;
}
public File getServerXml() {
return serverXml;
}
public void setServerXml(final File serverXml) {
this.serverXml = serverXml;
}
public boolean isKeepServerXmlAsThis() {
return keepServerXmlAsThis;
}
public void setKeepServerXmlAsThis(final boolean keepServerXmlAsThis) {
this.keepServerXmlAsThis = keepServerXmlAsThis;
}
public Properties getProperties() {
return properties;
}
public void setProperties(final Properties properties) {
this.properties = properties;
}
public boolean isQuickSession() {
return quickSession;
}
public void setQuickSession(final boolean quickSession) {
this.quickSession = quickSession;
}
public boolean isSkipHttp() {
return skipHttp;
}
public void setSkipHttp(final boolean skipHttp) {
this.skipHttp = skipHttp;
}
public boolean isSsl() {
return ssl;
}
public void setSsl(final boolean ssl) {
this.ssl = ssl;
}
public String getKeystoreFile() {
return keystoreFile;
}
public void setKeystoreFile(final String keystoreFile) {
this.keystoreFile = keystoreFile;
}
public String getKeystorePass() {
return keystorePass;
}
public void setKeystorePass(final String keystorePass) {
this.keystorePass = keystorePass;
}
public String getKeystoreType() {
return keystoreType;
}
public void setKeystoreType(final String keystoreType) {
this.keystoreType = keystoreType;
}
public String getClientAuth() {
return clientAuth;
}
public void setClientAuth(final String clientAuth) {
this.clientAuth = clientAuth;
}
public String getKeyAlias() {
return keyAlias;
}
public void setKeyAlias(final String keyAlias) {
this.keyAlias = keyAlias;
}
public String getSslProtocol() {
return sslProtocol;
}
public void setSslProtocol(final String sslProtocol) {
this.sslProtocol = sslProtocol;
}
public String getWebXml() {
return webXml;
}
public void setWebXml(final String webXml) {
this.webXml = webXml;
}
public LoginConfigBuilder getLoginConfig() {
return loginConfig;
}
public Builder loginConfig(final LoginConfigBuilder loginConfig) {
setLoginConfig(loginConfig);
return this;
}
public void setLoginConfig(final LoginConfigBuilder loginConfig) {
this.loginConfig = loginConfig;
}
public Collection<SecurityConstaintBuilder> getSecurityConstraints() {
return securityConstraints;
}
public Builder securityConstraints(final SecurityConstaintBuilder securityConstraint) {
securityConstraints = securityConstraints == null ? new ArrayList<>() : securityConstraints;
securityConstraints.add(securityConstraint);
return this;
}
public void setSecurityConstraints(final Collection<SecurityConstaintBuilder> securityConstraints) {
this.securityConstraints = securityConstraints;
}
public Realm getRealm() {
return realm;
}
public Builder realm(final Realm realm) {
setRealm(realm);
return this;
}
public void setRealm(final Realm realm) {
this.realm = realm;
}
public Map<String, String> getUsers() {
return users;
}
public void setUsers(final Map<String, String> users) {
this.users = users;
}
public Map<String, String> getRoles() {
return roles;
}
public void setRoles(final Map<String, String> roles) {
this.roles = roles;
}
public boolean isHttp2() {
return http2;
}
public void setHttp2(final boolean http2) {
this.http2 = http2;
}
public Collection<Connector> getConnectors() {
return connectors;
}
public String getTempDir() {
return tempDir;
}
public void setTempDir(final String tempDir) {
this.tempDir = tempDir;
}
public boolean isWebResourceCached() {
return webResourceCached;
}
public void setWebResourceCached(final boolean webResourceCached) {
this.webResourceCached = webResourceCached;
}
public String getConf() {
return conf;
}
public void setConf(final String conf) {
this.conf = conf;
}
public boolean isDeleteBaseOnStartup() {
return deleteBaseOnStartup;
}
public void setDeleteBaseOnStartup(final boolean deleteBaseOnStartup) {
this.deleteBaseOnStartup = deleteBaseOnStartup;
}
public String getJaxrsMapping() {
return jaxrsMapping;
}
public void setJaxrsMapping(final String jaxrsMapping) {
this.jaxrsMapping = jaxrsMapping;
}
public boolean isCdiConversation() {
return cdiConversation;
}
public void setCdiConversation(final boolean cdiConversation) {
this.cdiConversation = cdiConversation;
}
public Builder randomHttpPort() {
try (final ServerSocket serverSocket = new ServerSocket(0)) {
this.httpPort = serverSocket.getLocalPort();
} catch (final IOException e) {
throw new IllegalStateException(e);
}
return this;
}
public Builder randomHttpsPort() {
try (final ServerSocket serverSocket = new ServerSocket(0)) {
this.httpsPort = serverSocket.getLocalPort();
} catch (final IOException e) {
throw new IllegalStateException(e);
}
return this;
}
public Builder loadFrom(final String resource) {
try (final InputStream is = findStream(resource)) {
if (is != null) {
final Properties config = new Properties() {{
load(is);
}};
loadFromProperties(config);
}
return this;
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
public void setServerXml(final String file) {
if (file == null) {
serverXml = null;
} else {
final File sXml = new File(file);
if (sXml.exists()) {
serverXml = sXml;
}
}
}
public Builder property(final String key, final String value) {
properties.setProperty(key, value);
return this;
}
private InputStream findStream(final String resource) throws FileNotFoundException {
InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
if (stream == null) {
final File file = new File(resource);
if (file.exists()) {
return new FileInputStream(file);
}
}
return stream;
}
public Builder user(final String name, final String pwd) {
if (users == null) {
users = new HashMap<>();
}
this.users.put(name, pwd);
return this;
}
public Builder role(final String user, final String roles) {
if (this.roles == null) {
this.roles = new HashMap<>();
}
this.roles.put(user, roles);
return this;
}
public Builder cxfServletParam(final String key, final String value) {
if (this.cxfServletParams == null) {
this.cxfServletParams = new HashMap<>();
}
this.cxfServletParams.put(key, value);
return this;
}
public String getActiveProtocol() {
return isSkipHttp() ? "https" : "http";
}
public int getActivePort() {
return isSkipHttp() ? getHttpsPort() : getHttpPort();
}
public boolean isTomcatAutoSetup() {
return tomcatAutoSetup;
}
public void setTomcatAutoSetup(final boolean tomcatAutoSetup) {
this.tomcatAutoSetup = tomcatAutoSetup;
}
public boolean isUseShutdownHook() {
return useShutdownHook;
}
public void setUseShutdownHook(final boolean useShutdownHook) {
this.useShutdownHook = useShutdownHook;
}
public Builder noShutdownHook() {
setUseShutdownHook(false);
return this;
}
public boolean isTomcatWrapLoader() {
return tomcatWrapLoader;
}
public void setTomcatWrapLoader(final boolean tomcatWrapLoader) {
this.tomcatWrapLoader = tomcatWrapLoader;
}
public void addInstanceCustomizer(final Consumer<Tomcat> customizer) {
instanceCustomizers.add(customizer);
}
public Builder instanceCustomizer(final Consumer<Tomcat> customizer) {
addInstanceCustomizer(customizer);
return this;
}
public void addCustomizer(final Consumer<Builder> configurationCustomizer) {
configurationCustomizer.accept(this);
}
public void addGlobalContextCustomizer(final Consumer<Context> contextConfigurer) {
if (contextConfigurers == null) {
contextConfigurers = new ArrayList<>();
}
contextConfigurers.add(contextConfigurer);
}
public void addServletContextInitializer(final ServletContainerInitializer initializer) {
initializers.add(initializer);
}
public Collection<ServletContainerInitializer> getInitializers() {
return initializers;
}
public String getJaxrsDefaultProviders() {
return jaxrsDefaultProviders;
}
public void setJaxrsDefaultProviders(final String jaxrsDefaultProviders) {
this.jaxrsDefaultProviders = jaxrsDefaultProviders;
}
public boolean isUseLog4j2JulLogManager() {
return useLog4j2JulLogManager;
}
public void setUseLog4j2JulLogManager(final boolean useLog4j2JulLogManager) {
this.useLog4j2JulLogManager = useLog4j2JulLogManager;
}
public String getDefaultSSLHostConfigName() {
return defaultSSLHostConfigName;
}
public void setDefaultSSLHostConfigName(final String defaultSSLHostConfigName) {
this.defaultSSLHostConfigName = defaultSSLHostConfigName;
}
public void loadFromProperties(final Properties config) {
// filtering properties with system properties or themself
final Substitutor strSubstitutor = new Substitutor(emptyMap()) {
@Override
public String getOrDefault(final String key, final String or) {
final String property = System.getProperty(key);
return property == null ? config.getProperty(key, or) : or;
}
};
final ValueTransformers transformers = getExtension(ValueTransformers.class);
for (final String key : config.stringPropertyNames()) {
final String val = config.getProperty(key);
if (val == null || val.trim().isEmpty()) {
continue;
}
final String newVal = transformers.apply(strSubstitutor.replace(config.getProperty(key)));
if (!val.equals(newVal)) {
config.setProperty(key, newVal);
}
}
for (final Field field : Builder.class.getDeclaredFields()) {
final CliOption annotation = field.getAnnotation(CliOption.class);
if (annotation == null) {
continue;
}
final String name = field.getName();
Stream.of(Stream.of(annotation.name()), Stream.of(annotation.alias()))
.flatMap(a -> a)
.map(config::getProperty)
.filter(Objects::nonNull)
.findAny().ifPresent(val -> {
final Object toSet;
if (field.getType() == String.class) {
toSet = val;
} else if (field.getType() == int.class) {
if ("httpPort".equals(name) && "-1".equals(val)) { // special case in case of random port
randomHttpPort();
toSet = null;
} else {
toSet = Integer.parseInt(val);
}
} else if (field.getType() == boolean.class) {
toSet = Boolean.parseBoolean(val);
} else if (field.getType() == File.class) {
toSet = new File(val);
} else {
toSet = null;
}
if (toSet == null) { // handled elsewhere
return;
}
if (!field.isAccessible()) {
field.setAccessible(true);
}
try {
field.set(this, toSet);
} catch (final IllegalAccessException e) {
throw new IllegalStateException(e);
}
});
}
// not trivial types
for (final String prop : config.stringPropertyNames()) {
if (prop.startsWith("properties.")) {
property(prop.substring("properties.".length()), config.getProperty(prop));
} else if (prop.startsWith("users.")) {
user(prop.substring("users.".length()), config.getProperty(prop));
} else if (prop.startsWith("roles.")) {
role(prop.substring("roles.".length()), config.getProperty(prop));
} else if (prop.startsWith("cxf.servlet.params.")) {
cxfServletParam(prop.substring("cxf.servlet.params.".length()), config.getProperty(prop));
} else if (prop.startsWith("connector.")) { // created in container
property(prop, config.getProperty(prop));
} else if (prop.equals("realm")) {
final ObjectRecipe recipe = newRecipe(config.getProperty(prop));
for (final String realmConfig : config.stringPropertyNames()) {
if (realmConfig.startsWith("realm.")) {
recipe.setProperty(realmConfig.substring("realm.".length()), config.getProperty(realmConfig));
}
}
this.realm = Realm.class.cast(recipe.create());
} else if (prop.equals("login")) {
final ObjectRecipe recipe = newRecipe(LoginConfigBuilder.class.getName());
for (final String nestedConfig : config.stringPropertyNames()) {
if (nestedConfig.startsWith("login.")) {
recipe.setProperty(nestedConfig.substring("login.".length()), config.getProperty(nestedConfig));
}
}
loginConfig = LoginConfigBuilder.class.cast(recipe.create());
} else if (prop.equals("securityConstraint")) {
final ObjectRecipe recipe = newRecipe(SecurityConstaintBuilder.class.getName());
for (final String nestedConfig : config.stringPropertyNames()) {
if (nestedConfig.startsWith("securityConstraint.")) {
recipe.setProperty(nestedConfig.substring("securityConstraint.".length()), config.getProperty(nestedConfig));
}
}
securityConstraints.add(SecurityConstaintBuilder.class.cast(recipe.create()));
} else if (prop.equals("configurationCustomizer")) {
final ObjectRecipe recipe = newRecipe(prop);
for (final String nestedConfig : config.stringPropertyNames()) {
if (nestedConfig.startsWith(prop + '.')) {
recipe.setProperty(nestedConfig.substring(prop.length() + 2 /*dot*/), config.getProperty(nestedConfig));
}
}
addCustomizer(Consumer.class.cast(recipe.create()));
}
}
}
public <T> T bind(final T instance) {
final ValueTransformers transformers = getExtension(ValueTransformers.class);
Class<? extends Object> type = instance.getClass();
do {
Stream.of(type.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(CliOption.class))
.forEach(f -> {
final CliOption annotation = f.getAnnotation(CliOption.class);
String value = properties.getProperty(annotation.name());
if (value == null) {
value = Stream.of(annotation.alias()).map(properties::getProperty).findFirst().orElse(null);
if (value == null) {
return;
}
}
value = transformers.apply(value);
if (!f.isAccessible()) {
f.setAccessible(true);
}
final Class<?> t = f.getType();
try {
if (t == String.class) {
f.set(instance, value);
} else if (t == int.class) {
f.set(instance, Integer.parseInt(value));
} else if (t == boolean.class) {
f.set(instance, Boolean.parseBoolean(value));
} else {
throw new IllegalArgumentException("Unsupported type " + t);
}
} catch (final IllegalAccessException iae) {
throw new IllegalStateException(iae);
}
});
type = type.getSuperclass();
} while (type != Object.class);
return instance;
}
public boolean isInjectServletContainerInitializer() {
return injectServletContainerInitializer;
}
public void setInjectServletContainerInitializer(final boolean injectServletContainerInitializer) {
this.injectServletContainerInitializer = injectServletContainerInitializer;
}
public String getMeecrowaveProperties() {
return meecrowaveProperties;
}
public void setMeecrowaveProperties(final String meecrowaveProperties) {
this.meecrowaveProperties = meecrowaveProperties;
}
}
public static class ValueTransformers implements Function<String, String> {
private final Map<String, ValueTransformer> transformers = new HashMap<>();
@Override
public String apply(final String value) {
if (value.startsWith("decode:")) {
if (transformers.isEmpty()) { // lazy loading
transformers.put("Static3DES", new ValueTransformer() { // compatibility with tomee
private final SecretKeySpec key = new SecretKeySpec(new byte[]{
(byte) 0x76, (byte) 0x6F, (byte) 0xBA, (byte) 0x39, (byte) 0x31,
(byte) 0x2F, (byte) 0x0D, (byte) 0x4A, (byte) 0xA3, (byte) 0x90,
(byte) 0x55, (byte) 0xFE, (byte) 0x55, (byte) 0x65, (byte) 0x61,
(byte) 0x13, (byte) 0x34, (byte) 0x82, (byte) 0x12, (byte) 0x17,
(byte) 0xAC, (byte) 0x77, (byte) 0x39, (byte) 0x19}, "DESede");
@Override
public String name() {
return "Static3DES";
}
@Override
public String apply(final String encodedPassword) {
Objects.requireNonNull(encodedPassword, "value can't be null");
try {
final byte[] cipherText = Base64.getDecoder().decode(encodedPassword);
final Cipher cipher = Cipher.getInstance("DESede");
cipher.init(Cipher.DECRYPT_MODE, key);
return new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
});
for (final ValueTransformer t : ServiceLoader.load(ValueTransformer.class)) {
transformers.put(t.name(), t);
}
}
final String substring = value.substring("decode:".length());
final int sep = substring.indexOf(':');
if (sep < 0) {
throw new IllegalArgumentException("No transformer algorithm for " + value);
}
final String algo = substring.substring(0, sep);
return Objects.requireNonNull(transformers.get(algo), "No ValueTransformer for value '" + value + "'").apply(substring.substring(sep + 1));
}
return value;
}
}
public static class LoginConfigBuilder {
private final LoginConfig loginConfig = new LoginConfig();
public void setErrorPage(final String errorPage) {
loginConfig.setErrorPage(errorPage);
}
public void setLoginPage(final String loginPage) {
loginConfig.setLoginPage(loginPage);
}
public void setRealmName(final String realmName) {
loginConfig.setRealmName(realmName);
}
public void setAuthMethod(final String authMethod) {
loginConfig.setAuthMethod(authMethod);
}
public LoginConfigBuilder errorPage(final String errorPage) {
loginConfig.setErrorPage(errorPage);
return this;
}
public LoginConfigBuilder loginPage(final String loginPage) {
loginConfig.setLoginPage(loginPage);
return this;
}
public LoginConfigBuilder realmName(final String realmName) {
loginConfig.setRealmName(realmName);
return this;
}
public LoginConfigBuilder authMethod(final String authMethod) {
loginConfig.setAuthMethod(authMethod);
return this;
}
public LoginConfig build() {
return loginConfig;
}
public LoginConfigBuilder basic() {
return authMethod("BASIC");
}
public LoginConfigBuilder digest() {
return authMethod("DIGEST");
}
public LoginConfigBuilder clientCert() {
return authMethod("CLIENT-CERT");
}
public LoginConfigBuilder form() {
return authMethod("FORM");
}
}
public static class SecurityConstaintBuilder {
private final SecurityConstraint securityConstraint = new SecurityConstraint();
public SecurityConstaintBuilder authConstraint(final boolean authConstraint) {
securityConstraint.setAuthConstraint(authConstraint);
return this;
}
public SecurityConstaintBuilder displayName(final String displayName) {
securityConstraint.setDisplayName(displayName);
return this;
}
public SecurityConstaintBuilder userConstraint(final String constraint) {
securityConstraint.setUserConstraint(constraint);
return this;
}
public SecurityConstaintBuilder addAuthRole(final String authRole) {
securityConstraint.addAuthRole(authRole);
return this;
}
public SecurityConstaintBuilder addCollection(final String name, final String pattern, final String... methods) {
final SecurityCollection collection = new SecurityCollection();
collection.setName(name);
collection.addPattern(pattern);
for (final String httpMethod : methods) {
collection.addMethod(httpMethod);
}
securityConstraint.addCollection(collection);
return this;
}
public void setAuthConstraint(final boolean authConstraint) {
securityConstraint.setAuthConstraint(authConstraint);
}
public void setDisplayName(final String displayName) {
securityConstraint.setDisplayName(displayName);
}
public void setUserConstraint(final String userConstraint) {
securityConstraint.setUserConstraint(userConstraint);
}
public void setAuthRole(final String authRole) { // easier for config
addAuthRole(authRole);
}
// name:pattern:method1/method2
public void setCollection(final String value) { // for config
final String[] split = value.split(":");
if (split.length != 3 && split.length != 2) {
throw new IllegalArgumentException("Can't parse " + value + ", syntax is: name:pattern:method1/method2");
}
addCollection(split[0], split[1], split.length == 2 ? new String[0] : split[2].split("/"));
}
public SecurityConstraint build() {
return securityConstraint;
}
}
private static class InternalTomcat extends Tomcat {
private Connector connector;
private void server(final Server s) {
server = s;
connector = server != null && server.findServices().length > 0 && server.findServices()[0].findConnectors().length > 0 ?
server.findServices()[0].findConnectors()[0] : null;
}
Connector getRawConnector() {
return connector;
}
}
private static class TomcatWithFastSessionIDs extends InternalTomcat {
@Override
public void start() throws LifecycleException {
// Use fast, insecure session ID generation for all tests
final Server server = getServer();
for (final Service service : server.findServices()) {
final org.apache.catalina.Container e = service.getContainer();
for (final org.apache.catalina.Container h : e.findChildren()) {
for (final org.apache.catalina.Container c : h.findChildren()) {
Manager m = ((org.apache.catalina.Context) c).getManager();
if (m == null) {
m = new StandardManager();
org.apache.catalina.Context.class.cast(c).setManager(m);
}
if (m instanceof ManagerBase) {
ManagerBase.class.cast(m).setSecureRandomClass(
"org.apache.catalina.startup.FastNonSecureRandom");
}
}
}
}
super.start();
}
}
private static class QuickServerXmlParser extends DefaultHandler {
private static final SAXParserFactory FACTORY = SAXParserFactory.newInstance();
static {
FACTORY.setNamespaceAware(true);
FACTORY.setValidating(false);
}
private static final String STOP_KEY = "STOP";
private static final String HTTP_KEY = "HTTP";
private static final String SECURED_SUFFIX = "S";
private static final String HOST_KEY = "host";
private static final String DEFAULT_CONNECTOR_KEY = HTTP_KEY;
private static final String DEFAULT_HTTP_PORT = "8080";
private static final String DEFAULT_HTTPS_PORT = "8443";
private static final String DEFAULT_STOP_PORT = "8005";
private static final String DEFAULT_HOST = "localhost";
private final Map<String, String> values = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
private QuickServerXmlParser(final boolean useDefaults) {
if (useDefaults) {
values.put(STOP_KEY, DEFAULT_STOP_PORT);
values.put(HTTP_KEY, DEFAULT_HTTP_PORT);
values.put(HOST_KEY, DEFAULT_HOST);
}
}
@Override
public void startElement(final String uri, final String localName,
final String qName, final Attributes attributes) throws SAXException {
if ("Server".equalsIgnoreCase(localName)) {
final String port = attributes.getValue("port");
if (port != null) {
values.put(STOP_KEY, port);
} else {
values.put(STOP_KEY, DEFAULT_STOP_PORT);
}
} else if ("Connector".equalsIgnoreCase(localName)) {
String protocol = attributes.getValue("protocol");
if (protocol == null) {
protocol = DEFAULT_CONNECTOR_KEY;
} else if (protocol.contains("/")) {
protocol = protocol.substring(0, protocol.indexOf("/"));
}
final String port = attributes.getValue("port");
final String ssl = attributes.getValue("secure");
if (ssl == null || "false".equalsIgnoreCase(ssl)) {
values.put(protocol.toUpperCase(), port);
} else {
values.put(protocol.toUpperCase() + SECURED_SUFFIX, port);
}
} else if ("Host".equalsIgnoreCase(localName)) {
final String host = attributes.getValue("name");
if (host != null) {
values.put(HOST_KEY, host);
}
}
}
private static QuickServerXmlParser parse(final File serverXml) {
return parse(serverXml, true);
}
private static QuickServerXmlParser parse(final File serverXml, final boolean defaults) {
final QuickServerXmlParser handler = new QuickServerXmlParser(defaults);
try {
final SAXParser parser = FACTORY.newSAXParser();
parser.parse(serverXml, handler);
} catch (final Exception e) {
// no-op: using defaults
}
return handler;
}
public String http() {
return value(HTTP_KEY, DEFAULT_HTTP_PORT);
}
private String https() { // enough common to be exposed as method
return securedValue(HTTP_KEY, DEFAULT_HTTPS_PORT);
}
private String stop() {
return value(STOP_KEY, DEFAULT_STOP_PORT);
}
private String value(final String key, final String defaultValue) {
final String val = values.get(key);
if (val == null) {
return defaultValue;
}
return val;
}
private String securedValue(final String key, final String defaultValue) {
return value(key + SECURED_SUFFIX, defaultValue);
}
}
// there to be able to stack config later on without breaking all methods
public static class DeploymentMeta {
private final String context;
private final File docBase;
private final Consumer<Context> consumer;
public DeploymentMeta(final String context, final File docBase, final Consumer<Context> consumer) {
this.context = context;
this.docBase = docBase;
this.consumer = consumer;
}
}
// just to type it and allow some extensions to use a ServiceLoader
public interface ConfigurationCustomizer extends Consumer<Meecrowave.Builder> {
}
public interface InstanceCustomizer extends Consumer<Tomcat> {
}
public interface ContextCustomizer extends Consumer<Context> {
}
public interface MeecrowaveAwareContextCustomizer extends ContextCustomizer {
void setMeecrowave(Meecrowave meecrowave);
}
// since it is too early to have CDI and lookup the instance we must set it manually
public interface MeecrowaveAwareInstanceCustomizer extends InstanceCustomizer {
void setMeecrowave(Meecrowave meecrowave);
}
private static final class MeecrowaveContainerLoader extends URLClassLoader {
private MeecrowaveContainerLoader(final URL[] urls, final ClassLoader parent) {
super(urls, parent);
}
}
}