blob: e5545a7e8f2e9c3636eda2588a5116e3e63a33cb [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.runner;
import org.apache.catalina.Server;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardServer;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.ParseException;
import org.apache.meecrowave.Meecrowave;
import org.apache.meecrowave.configuration.Configuration;
import org.apache.meecrowave.runner.cli.CliOption;
import org.apache.xbean.recipe.ObjectRecipe;
import javax.enterprise.inject.Vetoed;
import java.io.File;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
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;
@Vetoed
public class Cli implements Runnable, AutoCloseable {
private final String[] args;
private volatile Meecrowave instance;
private volatile boolean closed;
public Cli(final String[] args) {
this.args = args;
}
public void run() {
final ParsedCommand parsedCommand = new ParsedCommand(args).invoke();
if (parsedCommand.isFailed()) {
return;
}
final Meecrowave.Builder builder = parsedCommand.getBuilder();
final CommandLine line = parsedCommand.getLine();
try (final Meecrowave meecrowave = new Meecrowave(builder)) {
synchronized (this) {
if (closed) {
return;
}
this.instance = meecrowave;
}
final String ctx = line.getOptionValue("context", "");
final String fixedCtx = !ctx.isEmpty() && !ctx.startsWith("/") ? '/' + ctx : ctx;
final String war = line.getOptionValue("webapp");
meecrowave.start();
if (war == null) {
meecrowave.deployClasspath(new Meecrowave.DeploymentMeta(
ctx,
ofNullable(line.getOptionValue("docbase")).map(File::new).orElseGet(() ->
Stream.of("base", "home")
.map(it -> System.getProperty("meecrowave." + it))
.filter(Objects::nonNull)
.map(it -> new File(it, "docBase"))
.filter(File::isDirectory)
.findFirst()
.orElse(null)),
null));
} else {
meecrowave.deployWebapp(fixedCtx, new File(war));
}
doWait(meecrowave, line);
}
}
protected void doWait(final Meecrowave meecrowave, final CommandLine line) {
meecrowave.getTomcat().getServer().await();
}
@Override
public synchronized void close() {
if (closed) {
return;
}
this.closed = true;
if (instance != null) {
final Server server = instance.getTomcat().getServer();
if (StandardServer.class.isInstance(server)) {
StandardServer.class.cast(server).stopAwait();
}
}
}
public static void main(final String[] args) {
new Cli(args).run();
}
// utility when user wraps the command, it enables him to manage the instance but reuse most of our options
public static Meecrowave.Builder create(final String[] args) {
final ParsedCommand command = new ParsedCommand(args).invoke();
if (command.isFailed()) {
return null;
}
return command.getBuilder();
}
private static void bind(final Meecrowave.Builder builder, final CommandLine line, final List<Field> fields, final Object config) {
fields.forEach(f -> {
final CliOption opt = f.getAnnotation(CliOption.class);
final Optional<String> first = Stream.of(Stream.of(opt.name()), Stream.of(opt.alias()))
.flatMap(a -> a)
.filter(line::hasOption)
.findFirst();
if (first.isPresent()) {
final String name = first.get();
ofNullable(f.getType() == boolean.class ?
ofNullable(line.getOptionValue(name)).map(Boolean::parseBoolean).orElse(true) :
toValue(builder, name, line.getOptionValues(name), f.getType()))
.ifPresent(v -> {
if (!f.isAccessible()) {
f.setAccessible(true);
}
try {
f.set(config, v);
} catch (final IllegalAccessException e) {
throw new IllegalStateException(e);
}
});
}
});
}
private static Object toValue(final Meecrowave.Builder builder, final String name, final String[] optionValues, final Class<?> type) {
if (optionValues == null || optionValues.length == 0) {
return null;
}
// decode options while it is strings
IntStream.range(0, optionValues.length)
.forEach(i -> optionValues[i] = builder.getExtension(Meecrowave.ValueTransformers.class).apply(optionValues[i]));
if (String.class == type) {
return optionValues[0];
}
if (int.class == type) {
return Integer.parseInt(optionValues[0]);
}
if (long.class == type) {
return Long.parseLong(optionValues[0]);
}
if (File.class == type) {
return new File(optionValues[0]);
}
if (Properties.class == type) {
final Properties props = new Properties();
Stream.of(optionValues).map(v -> v.split("=")).forEach(v -> props.setProperty(v[0], v[1]));
return props;
}
if (Map.class == type) {
final Map<String, String> props = new HashMap<>();
Stream.of(optionValues).map(v -> v.split("=")).forEach(v -> props.put(v[0], v[1]));
return props;
}
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
switch (name) {
case "realm": // org.foo.Impl:attr1=val1;attr2=val2
try {
int end = optionValues[0].indexOf(':');
if (end < 0) {
return loader.loadClass(optionValues[0]).newInstance();
}
final ObjectRecipe recipe = new ObjectRecipe(optionValues[0].substring(0, end));
Stream.of(optionValues[0].substring(end + 1, optionValues[0].length()).split(";"))
.map(v -> v.split("="))
.forEach(v -> recipe.setProperty(v[0], v[1]));
return recipe.create(loader);
} catch (final Exception cnfe) {
throw new IllegalArgumentException(optionValues[0]);
}
case "security-constraint": // attr1=val1;attr2=val2
return Stream.of(optionValues)
.map(item -> {
try {
final ObjectRecipe recipe = new ObjectRecipe(Meecrowave.SecurityConstaintBuilder.class);
Stream.of(item.split(";"))
.map(v -> v.split("="))
.forEach(v -> recipe.setProperty(v[0], v[1]));
return recipe.create(loader);
} catch (final Exception cnfe) {
throw new IllegalArgumentException(optionValues[0]);
}
}).collect(toList());
case "login-config":
try {
final ObjectRecipe recipe = new ObjectRecipe(Meecrowave.LoginConfigBuilder.class);
Stream.of(optionValues[0].split(";"))
.map(v -> v.split("="))
.forEach(v -> recipe.setProperty(v[0], v[1]));
return recipe.create(loader);
} catch (final Exception cnfe) {
throw new IllegalArgumentException(optionValues[0]);
}
case "connector": // org.foo.Impl:attr1=val1;attr2=val2
return Stream.of(optionValues)
.map(v -> {
try {
int end = v.indexOf(':');
if (end < 0) {
return new Connector(v);
}
final Connector connector = new Connector(optionValues[0].substring(0, end));
Stream.of(v.substring(end + 1, v.length()).split(";"))
.map(i -> i.split("="))
.forEach(i -> connector.setProperty(i[0], i[1]));
return connector;
} catch (final Exception cnfe) {
throw new IllegalArgumentException(optionValues[0]);
}
}).collect(toList());
default:
throw new IllegalArgumentException("Unsupported " + name);
}
}
public interface Options {
}
private static final class ParsedCommand {
private final String[] args;
private boolean failed;
private CommandLine line;
private Meecrowave.Builder builder;
private ParsedCommand(final String... args) {
this.args = args;
}
private static void help(final org.apache.commons.cli.Options options) {
new HelpFormatter().printHelp("java -jar meecrowave-runner.jar", options);
}
private static Meecrowave.Builder buildConfig(final CommandLine line, final List<Field> fields,
final Map<Object, List<Field>> propertiesOptions) {
final Meecrowave.Builder config = new Meecrowave.Builder();
bind(config, line, fields, config);
propertiesOptions.forEach((o, f) -> {
bind(config, line, f, o);
config.setExtension(o.getClass(), o);
});
return config;
}
boolean isFailed() {
return failed;
}
public CommandLine getLine() {
return line;
}
public Meecrowave.Builder getBuilder() {
return builder;
}
public ParsedCommand invoke() {
final org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();
options.addOption(null, "help", false, "Show help");
options.addOption(null, "context", true, "The context to use to deploy the webapp");
options.addOption(null, "webapp", true, "Location of the webapp, if not set the classpath will be deployed");
options.addOption(null, "docbase", true, "Location of the docbase for a classpath deployment");
final List<Field> fields = Stream.of(Configuration.class.getDeclaredFields())
.filter(f -> f.isAnnotationPresent(CliOption.class))
.collect(toList());
final Map<Object, List<Field>> propertiesOptions = StreamSupport.stream(ServiceLoader.load(Options.class).spliterator(), false)
.collect(toMap(identity(), o -> Stream.of(o.getClass().getDeclaredFields()).filter(f -> f.isAnnotationPresent(CliOption.class)).collect(toList())));
fields.forEach(f -> {
final CliOption opt = f.getAnnotation(CliOption.class);
final String description = opt.description();
options.addOption(null, opt.name(), true /*even for booleans otherwise no way to set false for true by default ones*/, description);
Stream.of(opt.alias()).forEach(a -> options.addOption(null, a, true, description));
});
propertiesOptions.values().forEach(all -> all.forEach(f -> {
final CliOption opt = f.getAnnotation(CliOption.class);
final String description = opt.description();
options.addOption(null, opt.name(), true, description);
Stream.of(opt.alias()).forEach(a -> options.addOption(null, a, true, description));
}));
final CommandLineParser parser = new DefaultParser();
try {
line = parser.parse(options, args, true);
} catch (final ParseException exp) {
help(options);
failed = true;
return this;
}
if (line.hasOption("help")) {
help(options);
failed = true;
return this;
}
builder = buildConfig(line, fields, propertiesOptions);
failed = false;
return this;
}
}
}