diff --git a/core/pom.xml b/core/pom.xml
index 208e8814..5885adbb 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -24,6 +24,8 @@
1.2.6
${project.build.directory}${file.separator}wildfly
+ ${project.build.testOutputDirectory}
+ -Dmaven.repo.local=${settings.localRepository}
@@ -92,6 +94,19 @@
+
+
+
+ src/test/resources
+ ${project.build.testOutputDirectory}
+
+
+
+ src/test/modules
+ true
+ ${jboss.home}/modules
+
+
maven-jar-plugin
@@ -119,6 +134,7 @@
wildfly@maven(org.jboss.universe:community-universe)#${version.org.wildfly}
+ false
${project.build.directory}/wildfly
${plugin.fork.embedded}
@@ -127,6 +143,21 @@
+
+
+ maven-resources-plugin
+
+
+ copy-module
+ pre-integration-test
+
+ testResources
+
+
+
+
maven-surefire-plugin
@@ -141,6 +172,7 @@
${maven.test.redirectTestOutputToFile}
${jboss.home}
+ ${test.jvm.args}
diff --git a/core/src/main/java/org/wildfly/plugin/core/Utils.java b/core/src/main/java/org/wildfly/plugin/core/Utils.java
index 47d4f529..1fc7c8b3 100644
--- a/core/src/main/java/org/wildfly/plugin/core/Utils.java
+++ b/core/src/main/java/org/wildfly/plugin/core/Utils.java
@@ -6,12 +6,17 @@
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
*
* @author jdenise@redhat.com
*/
public class Utils {
+ private static final Pattern WHITESPACE_IF_NOT_QUOTED = Pattern.compile("(\\S+\"[^\"]+\")|\\S+");
public static boolean isValidHomeDirectory(final Path path) {
return path != null
@@ -19,4 +24,24 @@ public static boolean isValidHomeDirectory(final Path path) {
&& Files.isDirectory(path)
&& Files.exists(path.resolve("jboss-modules.jar"));
}
+
+ /**
+ * Splits the arguments into a list. The arguments are split based on
+ * whitespace while ignoring whitespace that is within quotes.
+ *
+ * @param arguments the arguments to split
+ *
+ * @return the list of the arguments
+ */
+ public static List splitArguments(final CharSequence arguments) {
+ final List args = new ArrayList<>();
+ final Matcher m = WHITESPACE_IF_NOT_QUOTED.matcher(arguments);
+ while (m.find()) {
+ final String value = m.group();
+ if (!value.isEmpty()) {
+ args.add(value);
+ }
+ }
+ return args;
+ }
}
diff --git a/core/src/main/java/org/wildfly/plugins/core/bootablejar/BootLoggingConfiguration.java b/core/src/main/java/org/wildfly/plugins/core/bootablejar/BootLoggingConfiguration.java
new file mode 100644
index 00000000..a00d567a
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/bootablejar/BootLoggingConfiguration.java
@@ -0,0 +1,1076 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.wildfly.plugins.core.bootablejar;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.jboss.as.controller.client.ModelControllerClient;
+import org.jboss.as.controller.client.helpers.ClientConstants;
+import org.jboss.as.controller.client.helpers.Operations;
+import org.jboss.dmr.ModelNode;
+import org.jboss.dmr.ModelType;
+import org.jboss.dmr.Property;
+
+/**
+ * Generates a new {@code logging.properties} file based on the logging subsystem model.
+ *
+ *
+ * This should be considered a hack which generates a {@code logging.properties} file. The generated file will not
+ * necessarily be identical to that of which WildFly generates. Expressions will be written to the generated file. For
+ * this reason a new file will be generated which the entry point needs to load as system properties before the log
+ * manager is configured.
+ *
+ *
+ *
+ * Also handlers, formatters and filters considered explicit will not be configured at boot. As they are not used by
+ * another resource this should not be an issue. Once the logging subsystems runtime phase is executed these resources
+ * will be initialized.
+ *
+ *
+ *
+ * The generated file cannot support log4j appenders created as custom-handlers. Boot errors will
+ * occur if this happens.
+ *
+ *
+ * @author James R. Perkins
+ */
+// @TODO, we can't use AbstractLogEnabled, it is not in the maven plugin classloader.
+public class BootLoggingConfiguration { // extends AbstractLogEnabled {
+
+ private static final Pattern SIZE_PATTERN = Pattern.compile("(\\d+)([kKmMgGbBtT])?");
+ private static final String NEW_LINE = System.lineSeparator();
+
+ private static final Collection IGNORED_PROPERTIES = Arrays.asList(
+ "java.ext.dirs",
+ "java.home",
+ "jboss.home.dir",
+ "java.io.tmpdir",
+ "jboss.controller.temp.dir",
+ "jboss.server.base.dir",
+ "jboss.server.config.dir",
+ "jboss.server.data.dir",
+ "jboss.server.default.config",
+ "jboss.server.deploy.dir",
+ "jboss.server.log.dir",
+ "jboss.server.persist.config",
+ "jboss.server.management.uuid",
+ "jboss.server.temp.dir",
+ "modules.path",
+ "org.jboss.server.bootstrap.maxThreads",
+ "user.dir",
+ "user.home");
+ private static final String KEY_OVERRIDES = "keyOverrides";
+ private final Map properties;
+ private final Map usedProperties;
+ private final Map additionalPatternFormatters;
+ private ModelControllerClient client;
+
+ public BootLoggingConfiguration() {
+ properties = new HashMap<>();
+ usedProperties = new TreeMap<>();
+ additionalPatternFormatters = new LinkedHashMap<>();
+ }
+
+ public void generate(final Path configDir, final ModelControllerClient client) throws Exception {
+ properties.clear();
+ usedProperties.clear();
+ additionalPatternFormatters.clear();
+ // First we need to determine if there is a logging subsystem, if not we don't need to handle rewriting the
+ // configuration.
+ ModelNode op = Operations.createOperation("read-children-names");
+ op.get(ClientConstants.CHILD_TYPE).set("subsystem");
+ ModelNode result = client.execute(op);
+ if (!Operations.isSuccessfulOutcome(result)) {
+ throw new Exception("Could not determine if the logging subsystem was present: "
+ + Operations.getFailureDescription(result).asString());
+ } else {
+ if (Operations.readResult(result)
+ .asList()
+ .stream()
+ .noneMatch((name) -> name.asString().equals("logging"))) {
+ return;
+ }
+ }
+ // Create the operations to read the resources required
+ final Operations.CompositeOperationBuilder builder = Operations.CompositeOperationBuilder.create()
+ .addStep(Operations.createReadResourceOperation(Operations.createAddress("subsystem", "logging"), true));
+ op = Operations.createOperation("read-children-resources");
+ op.get(ClientConstants.CHILD_TYPE).set("system-property");
+ builder.addStep(op);
+ op = Operations.createOperation("read-children-resources");
+ op.get(ClientConstants.CHILD_TYPE).set("path");
+ builder.addStep(op);
+
+ result = client.execute(builder.build());
+ if (!Operations.isSuccessfulOutcome(result)) {
+ throw new Exception("Failed to determine the logging configuration: "
+ + Operations.getFailureDescription(result).asString());
+ }
+ result = Operations.readResult(result);
+ // step-1 is the subsystem, step-2 is the system properties and step-3 is the paths
+ final ModelNode subsystem = Operations.readResult(result.get("step-1"));
+ final ModelNode systemProperties = Operations.readResult(result.get("step-2"));
+ final ModelNode paths = Operations.readResult(result.get("step-3"));
+
+ // This shouldn't happen, but let's be safe
+ if (subsystem.isDefined()) {
+ // Sets the client to use
+ this.client = client;
+ parseProperties(systemProperties);
+ try (BufferedWriter writer = Files.newBufferedWriter(configDir.resolve("logging.properties"),
+ StandardCharsets.UTF_8)) {
+ writer.write("# Note this file has been generated and will be overwritten if a");
+ writer.write(NEW_LINE);
+ writer.write("# logging subsystem has been defined in the XML configuration.");
+ writer.write(NEW_LINE);
+ writer.write(NEW_LINE);
+
+ writeLoggers(writer, subsystem);
+ writeHandlers(writer, subsystem, paths);
+ // Note the formatters MUST be written after the handlers. Handlers have a legacy "formatter" attribute and
+ // additional pattern-formatters may need be written.
+ writeFormatters(writer, subsystem);
+ writeFilters(writer, subsystem);
+ } catch (IOException e) {
+ throw new Exception("Failed to write the logging configuration file to " + configDir.toAbsolutePath(), e);
+ }
+
+ // Collect the properties we need at boot
+ final Properties requiredProperties = new Properties();
+ final Iterator> iter = usedProperties.entrySet().iterator();
+ while (iter.hasNext()) {
+ final Map.Entry entry = iter.next();
+ final String key = entry.getKey();
+ if (properties.containsKey(key)) {
+ requiredProperties.put(key, properties.get(key));
+ } else {
+ // @TODO, we can't use AbstractLogEnabled, it is not in the maven plugin classloader.
+ // getLogger().warn(String.format("The value for the expression \"%s\" could not be resolved " +
+ // "and may not be set at boot if no default value is available.", entry.getValue()));
+ System.err.println(String.format("The value for the expression \"%s\" could not be resolved "
+ + "and may not be set at boot if no default value is available.", entry.getValue()));
+ }
+ iter.remove();
+ }
+
+ if (!requiredProperties.isEmpty()) {
+ // Note the hard-coded "boot-config.properties", the bootable JAR entry point will look for this file
+ // and process it if it exists.
+ try (BufferedWriter writer = Files.newBufferedWriter(configDir.resolve("boot-config.properties"))) {
+ requiredProperties.store(writer, "Bootable JAR boot properties required by the log manager.");
+ } catch (IOException e) {
+ throw new Exception("Failed to write the system properties required by the logging configuration file to "
+ + configDir.toAbsolutePath(), e);
+ }
+ }
+ }
+ }
+
+ private void writeFilters(final Writer writer, final ModelNode subsystem) throws IOException {
+ if (subsystem.hasDefined("filter")) {
+ for (Property property : subsystem.get("filter").asPropertyList()) {
+ final String name = property.getName();
+ final ModelNode model = property.getValue();
+ final String prefix = "filter." + name;
+ writeProperty(writer, prefix, null, resolveAsString(model.get("class")));
+ writeProperty(writer, prefix, "module", resolveAsString(model.get("module")));
+
+ final ModelNode allProperties = new ModelNode();
+
+ if (model.hasDefined("constructor-properties")) {
+ final ModelNode properties = model.get("constructor-properties");
+ final Collection constructorNames = properties.asPropertyList()
+ .stream()
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ writeProperty(writer, prefix, "constructorProperties", toCsvString(constructorNames));
+ for (String n : constructorNames) {
+ allProperties.get(n).set(properties.get(n));
+ }
+ }
+ if (model.hasDefined("properties")) {
+ final ModelNode properties = model.get("properties");
+ final Collection propertyNames = properties.asPropertyList()
+ .stream()
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ for (String n : propertyNames) {
+ allProperties.get(n).set(properties.get(n));
+ }
+ }
+ if (allProperties.isDefined()) {
+ writeProperty(writer, prefix, "properties", toCsvString(allProperties.asPropertyList()
+ .stream()
+ .map(Property::getName)
+ .collect(Collectors.toList())));
+ writeProperties(writer, prefix, allProperties);
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeFormatters(final Writer writer, final ModelNode subsystem) throws IOException {
+ // Formatters
+ if (subsystem.hasDefined("custom-formatter")) {
+ writeCustomFormatter(writer, subsystem.get("custom-formatter").asPropertyList());
+ }
+ if (subsystem.hasDefined("json-formatter")) {
+ writeStructuredFormatter("org.jboss.logmanager.formatters.JsonFormatter", writer,
+ subsystem.get("json-formatter").asPropertyList());
+ }
+ if (subsystem.hasDefined("pattern-formatter")) {
+ writePatternFormatter(writer, subsystem.get("pattern-formatter").asPropertyList());
+ }
+ if (subsystem.hasDefined("xml-formatter")) {
+ writeStructuredFormatter("org.jboss.logmanager.formatters.XmlFormatter", writer,
+ subsystem.get("xml-formatter").asPropertyList());
+ }
+ }
+
+ private void writeCustomFormatter(final Writer writer, final List formatters) throws IOException {
+ for (Property property : formatters) {
+ final String name = property.getName();
+ final ModelNode model = property.getValue().clone();
+ final String prefix = "formatter." + name;
+ writeProperty(writer, prefix, null, resolveAsString(model.remove("class")));
+ writeProperty(writer, prefix, "module", resolveAsString(model.remove("module")));
+ if (model.hasDefined("properties")) {
+ final ModelNode properties = model.get("properties");
+ // Next we need to write the properties
+ final Collection definedPropertyNames = properties.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames));
+ // Write the property values
+ for (String attributeName : definedPropertyNames) {
+ writeProperty(writer, prefix, attributeName, properties.get(attributeName));
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writePatternFormatter(final Writer writer, final List formatters) throws IOException {
+ for (Property property : formatters) {
+ final String name = property.getName();
+ final ModelNode model = property.getValue().clone();
+ final String prefix = "formatter." + name;
+ writeProperty(writer, prefix, null, "org.jboss.logmanager.formatters.PatternFormatter");
+
+ // Next we need to write the properties
+ final Collection definedPropertyNames = model.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames
+ .stream()
+ .map(BootLoggingConfiguration::resolvePropertyName)
+ .collect(Collectors.toList())));
+ // Write the property values
+ for (String attributeName : definedPropertyNames) {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), model.get(attributeName));
+ }
+ writer.write(NEW_LINE);
+ }
+
+ // Write any additional pattern-formatters that were defined on a handlers "formatter" attribute
+ final Iterator> iter = additionalPatternFormatters.entrySet().iterator();
+ while (iter.hasNext()) {
+ final Map.Entry entry = iter.next();
+ final String prefix = "formatter." + entry.getKey();
+ writeProperty(writer, prefix, null, "org.jboss.logmanager.formatters.PatternFormatter");
+ writeProperty(writer, prefix, "constructorProperties", "pattern");
+ writeProperty(writer, prefix, "properties", "pattern");
+ writeProperty(writer, prefix, "pattern", entry.getValue());
+ writer.write(NEW_LINE);
+ iter.remove();
+ }
+ }
+
+ private void writeStructuredFormatter(final String type, final Writer writer,
+ final List formatters) throws IOException {
+ for (Property property : formatters) {
+ final String name = property.getName();
+ final ModelNode model = property.getValue().clone();
+ final String prefix = "formatter." + name;
+ writeProperty(writer, prefix, null, type);
+ boolean needKeyOverrides = !model.hasDefined("key-overrides");
+ // The key-overrides are used as constructor parameters
+ // This property is alwasy added.
+ writeProperty(writer, prefix, "constructorProperties", KEY_OVERRIDES);
+ // Next we need to write the properties
+ final Collection definedPropertyNames = model.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ if (needKeyOverrides) {
+ definedPropertyNames.add(KEY_OVERRIDES);
+ }
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames
+ .stream()
+ .map(BootLoggingConfiguration::resolvePropertyName)
+ .collect(Collectors.toList())));
+ // Write the property values
+ for (String attributeName : definedPropertyNames) {
+ final ModelNode value = model.get(attributeName);
+ // Handle special cases
+ if ("exception-output-type".equals(attributeName)) {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), toEnumString(model.get(attributeName)));
+ } else {
+ if (needKeyOverrides && KEY_OVERRIDES.equals(attributeName)) {
+ // The value is empty if explicitely added.
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), "");
+ } else {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), value);
+ }
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeHandlers(final Writer writer, final ModelNode subsystem, final ModelNode pathModel) throws IOException {
+ if (subsystem.hasDefined("async-handler")) {
+ writeAsyncHandlers(writer, subsystem.get("async-handler").asPropertyList());
+ }
+
+ if (subsystem.hasDefined("console-handler")) {
+ writeConsoleHandlers(writer, subsystem.get("console-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("custom-handler")) {
+ writeCustomHandlers(writer, subsystem.get("custom-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("file-handler")) {
+ writeFileHandlers(pathModel, "org.jboss.logmanager.handlers.FileHandler", writer,
+ subsystem.get("file-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("periodic-rotating-file-handler")) {
+ writeFileHandlers(pathModel, "org.jboss.logmanager.handlers.PeriodicRotatingFileHandler", writer,
+ subsystem.get("periodic-rotating-file-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("periodic-size-rotating-file-handler")) {
+ writeFileHandlers(pathModel, "org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler", writer,
+ subsystem.get("periodic-size-rotating-file-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("size-rotating-file-handler")) {
+ writeFileHandlers(pathModel, "org.jboss.logmanager.handlers.SizeRotatingFileHandler", writer,
+ subsystem.get("size-rotating-file-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("socket-handler")) {
+ writeSocketHandler(writer, subsystem.get("socket-handler").asPropertyList());
+ }
+ if (subsystem.hasDefined("syslog-handler")) {
+ writeSyslogHandler(writer, subsystem.get("syslog-handler").asPropertyList());
+ }
+ }
+
+ private void writeAsyncHandlers(final Writer writer, final List handlers) throws IOException {
+ for (Property property : handlers) {
+ final String name = property.getName();
+ final String prefix = "handler." + name;
+ final ModelNode model = property.getValue().clone();
+ writeCommonHandler("org.jboss.logmanager.handlers.AsyncHandler", writer, name, prefix, model);
+ final ModelNode subhandlers = model.remove("subhandlers");
+ if (isDefined(subhandlers)) {
+ writeProperty(writer, prefix, "handlers", subhandlers);
+ }
+ // Next we need to write the properties
+ final Collection definedPropertyNames = model.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ definedPropertyNames.add("closeChildren");
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames
+ .stream()
+ .map(BootLoggingConfiguration::resolvePropertyName)
+ .collect(Collectors.toList())));
+ // Write the constructor properties
+ writeProperty(writer, prefix, "constructorProperties", "queueLength");
+ // Write the property values
+ for (String attributeName : definedPropertyNames) {
+ if ("closeChildren".equals(attributeName)) {
+ writeProperty(writer, prefix, attributeName, "false");
+ } else {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), model.get(attributeName));
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeConsoleHandlers(final Writer writer, final List handlers) throws IOException {
+ for (Property property : handlers) {
+ final String name = property.getName();
+ final String prefix = "handler." + name;
+ final ModelNode model = property.getValue().clone();
+ writeCommonHandler("org.jboss.logmanager.handlers.ConsoleHandler", writer, name, prefix, model);
+ // Next we need to write the properties
+ final Collection definedPropertyNames = model.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames
+ .stream()
+ .map(BootLoggingConfiguration::resolvePropertyName)
+ .collect(Collectors.toList())));
+ // Write the property values
+ for (String attributeName : definedPropertyNames) {
+ if ("target".equals(attributeName)) {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), toEnumString(model.get(attributeName)));
+ } else {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), model.get(attributeName));
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeCustomHandlers(final Writer writer, final List handlers) throws IOException {
+ for (Property property : handlers) {
+ final String name = property.getName();
+ final String prefix = "handler." + name;
+ final ModelNode model = property.getValue().clone();
+ writeCommonHandler(null, writer, name, prefix, model);
+ // Next we need to write the properties
+ if (model.hasDefined("properties")) {
+ final Collection definedPropertyNames = model.get("properties").asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ if (model.hasDefined("enabled")) {
+ definedPropertyNames.add("enabled");
+ }
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames));
+ final ModelNode properties = model.get("properties");
+ for (String attributeName : definedPropertyNames) {
+ if ("enabled".equals(attributeName)) {
+ if (model.hasDefined(attributeName)) {
+ writeProperty(writer, prefix, attributeName, model.get(attributeName));
+ }
+ } else {
+ writeProperty(writer, prefix, attributeName, properties.get(attributeName));
+ }
+ }
+ } else {
+ if (model.hasDefined("enabled")) {
+ writeProperty(writer, prefix, "properties", "enabled");
+ writeProperty(writer, prefix, "enabled", model.get("enabled"));
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeFileHandlers(final ModelNode pathModel, final String type, final Writer writer,
+ final List handlers) throws IOException {
+ for (Property property : handlers) {
+ final String name = property.getName();
+ final String prefix = "handler." + name;
+ final ModelNode model = property.getValue().clone();
+
+ final ModelNode file = model.remove("file");
+ // If the file is not defined, which shouldn't happen, we'll just skip this one
+ if (!isDefined(file)) {
+ continue;
+ }
+
+ writeCommonHandler(type, writer, name, prefix, model);
+
+ // Next we need to write the properties
+ final Collection definedPropertyNames = model.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ final Collection propertyNames = definedPropertyNames
+ .stream()
+ .map(BootLoggingConfiguration::resolvePropertyName)
+ .collect(Collectors.toList());
+ propertyNames.add("fileName");
+ writeProperty(writer, prefix, "properties", toCsvString(propertyNames));
+
+ // Write the constructor properties
+ writeProperty(writer, prefix, "constructorProperties", "fileName,append");
+
+ // Write the remainder of the properties
+ for (String attributeName : definedPropertyNames) {
+ // The rotate-size requires special conversion
+ if ("rotate-size".equals(attributeName)) {
+ final String resolvedValue = String.valueOf(parseSize(model.get(attributeName)));
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), resolvedValue);
+ } else {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), model.get(attributeName));
+ }
+ }
+
+ // Write the fileName
+ final StringBuilder result = new StringBuilder();
+ if (file.hasDefined("relative-to")) {
+ final String relativeTo = file.get("relative-to").asString();
+ resolveRelativeTo(pathModel, relativeTo, result);
+ }
+ if (file.hasDefined("path")) {
+ result.append(resolveAsString(file.get("path")));
+ }
+ writeProperty(writer, prefix, "fileName", result.toString());
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeSocketHandler(final Writer writer, final List handlers) throws IOException {
+ // Socket handlers are actually configured late initialized defined as a DelayedHandler
+ for (Property property : handlers) {
+ final String name = property.getName();
+ final String prefix = "handler." + name;
+ final ModelNode model = property.getValue().clone();
+ writeCommonHandler("org.jboss.logmanager.handlers.DelayedHandler", writer, name, prefix, model);
+ if (model.hasDefined("enabled")) {
+ writeProperty(writer, prefix, "properties", "enabled");
+ writeProperty(writer, prefix, "enabled", model.get("enabled"));
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeSyslogHandler(final Writer writer, final List handlers) throws IOException {
+ // Socket handlers are actually configured late initialized defined as a DelayedHandler
+ for (Property property : handlers) {
+ final String name = property.getName();
+ final String prefix = "handler." + name;
+ final ModelNode model = property.getValue().clone();
+ writeCommonHandler("org.jboss.logmanager.handlers.SyslogHandler", writer, name, prefix, model);
+
+ // Next we need to write the properties
+ final Collection definedPropertyNames = model.asPropertyList()
+ .stream()
+ .filter((p) -> p.getValue().isDefined())
+ .map(Property::getName)
+ .collect(Collectors.toList());
+ writeProperty(writer, prefix, "properties", toCsvString(definedPropertyNames
+ .stream()
+ .map(BootLoggingConfiguration::resolvePropertyName)
+ .collect(Collectors.toList())));
+ for (String attributeName : definedPropertyNames) {
+ if ("facility".equals(attributeName)) {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), toEnumString(model.get(attributeName)));
+ } else {
+ writeProperty(writer, prefix, resolvePropertyName(attributeName), model.get(attributeName));
+ }
+ }
+ writer.write(NEW_LINE);
+ }
+ }
+
+ private void writeCommonHandler(final String type, final Writer writer, final String name,
+ final String prefix, final ModelNode model) throws IOException {
+ if (type == null) {
+ writeProperty(writer, prefix, null, resolveAsString(model.remove("class")));
+ writeProperty(writer, prefix, "module", resolveAsString(model.remove("module")));
+ } else {
+ writeProperty(writer, prefix, null, type);
+ }
+
+ // Remove the legacy "name" attribute
+ model.remove("name");
+
+ // Write the level
+ final ModelNode level = model.remove("level");
+ if (isDefined(level)) {
+ writeProperty(writer, prefix, "level", level);
+ }
+ final ModelNode encoding = model.remove("encoding");
+ if (isDefined(encoding)) {
+ writeProperty(writer, prefix, "encoding", encoding);
+ }
+
+ final ModelNode namedFormatter = model.remove("named-formatter");
+ final ModelNode formatter = model.remove("formatter");
+ if (isDefined(namedFormatter)) {
+ writeProperty(writer, prefix, "formatter", namedFormatter.asString());
+ } else if (isDefined(formatter)) {
+ // We need to add a formatter with the known name used in WildFly
+ final String defaultFormatterName = name + "-wfcore-pattern-formatter";
+ additionalPatternFormatters.put(defaultFormatterName, resolveAsString(formatter));
+ writeProperty(writer, prefix, "formatter", defaultFormatterName);
+ }
+ // Write the filter spec and remove the filter attribute which we will not use
+ model.remove("filter");
+ final ModelNode filter = model.remove("filter-spec");
+ if (isDefined(filter)) {
+ writeProperty(writer, prefix, "filter", filter);
+ }
+ }
+
+ private void writeLoggers(final Writer writer, final ModelNode model) throws IOException {
+ if (model.hasDefined("logger")) {
+ final List loggerModel = model.get("logger").asPropertyList();
+ writer.write("# Additional loggers to configure (the root logger is always configured)");
+ writer.write(NEW_LINE);
+ // First we need to list the loggers to define
+ writeProperty(writer, "loggers", null, toCsvString(loggerModel
+ .stream()
+ .map(Property::getName)
+ .collect(Collectors.toList())));
+ writer.write(NEW_LINE);
+ // Next get the root logger
+ if (model.hasDefined("root-logger", "ROOT")) {
+ writeLogger(writer, null, model.get("root-logger", "ROOT"));
+ }
+
+ for (Property property : loggerModel) {
+ writeLogger(writer, property.getName(), property.getValue());
+ }
+ }
+ }
+
+ private void writeLogger(final Writer writer, final String name, final ModelNode model) throws IOException {
+ final String prefix = name == null ? "logger" : "logger." + name;
+ if (model.hasDefined("filter-spec")) {
+ writeProperty(writer, prefix, "filter", model.get("filter-spec"));
+ }
+ if (model.hasDefined("handlers")) {
+ writeProperty(writer, prefix, "handlers", toCsvString(model.get("handlers").asList()
+ .stream()
+ .map(ModelNode::asString)
+ .collect(Collectors.toList())));
+ }
+ if (model.hasDefined("level")) {
+ writeProperty(writer, prefix, "level", model.get("level"));
+ }
+ if (model.hasDefined("use-parent-filters")) {
+ writeProperty(writer, prefix, "useParentFilters", model.get("use-parent-filters"));
+ }
+ if (model.hasDefined("use-parent-handlers")) {
+ writeProperty(writer, prefix, "useParentHandlers", model.get("use-parent-handlers"));
+ }
+ writer.write(NEW_LINE);
+ }
+
+ private void writeProperties(final Writer writer, final String prefix, final ModelNode model) throws IOException {
+ for (Property property : model.asPropertyList()) {
+ final String name = property.getName();
+ final ModelNode value = property.getValue();
+ if (value.isDefined()) {
+ writeProperty(writer, prefix, name, value);
+ }
+ }
+ }
+
+ private void writeProperty(final Writer out, final String prefix, final String name, final ModelNode value)
+ throws IOException {
+ writeProperty(out, prefix, name, resolveAsString(value));
+ }
+
+ private String toEnumString(final ModelNode value) {
+ final StringBuilder result = new StringBuilder();
+ if (value.getType() == ModelType.EXPRESSION) {
+ final Collection expressions = Expression.parse(value.asExpression());
+ for (Expression expression : expressions) {
+ addUsedProperties(expression, value.asString());
+ result.append("${");
+ final Iterator iter = expression.getKeys().iterator();
+ while (iter.hasNext()) {
+ result.append(iter.next());
+ if (iter.hasNext()) {
+ result.append(',');
+ }
+ }
+ if (expression.hasDefault()) {
+ result.append(':');
+ final String dft = expression.getDefaultValue();
+ for (char c : dft.toCharArray()) {
+ if (c == '-' || c == '.') {
+ result.append('_');
+ } else {
+ result.append(Character.toUpperCase(c));
+ }
+ }
+ }
+ result.append('}');
+ }
+ } else {
+ for (char c : value.asString().toCharArray()) {
+ if (c == '-' || c == '.') {
+ result.append('_');
+ } else {
+ result.append(Character.toUpperCase(c));
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ private String resolveAsString(final ModelNode value) {
+ if (value.getType() == ModelType.LIST) {
+ return toCsvString(value.asList()
+ .stream()
+ .map(ModelNode::asString)
+ .collect(Collectors.toList()));
+ } else if (value.getType() == ModelType.OBJECT) {
+ return modelToMap(value);
+ } else {
+ if (value.getType() == ModelType.EXPRESSION) {
+ final Collection expressions = Expression.parse(value.asExpression());
+ addUsedProperties(expressions, value.asString());
+ }
+ return value.asString();
+ }
+ }
+
+ private long parseSize(final ModelNode value) throws IOException {
+ String stringValue;
+ // This requires some special handling as we need the resolved value.
+ if (value.getType() == ModelType.EXPRESSION) {
+ // We need update the usedProperties
+ final Collection expressions = Expression.parse(value.asExpression());
+ addUsedProperties(expressions, value.asString());
+ // Now we need to resolve the expression
+ final ModelNode op = Operations.createOperation("resolve-expression");
+ op.get("expression").set(value.asString());
+ final ModelNode result = client.execute(op);
+ if (!Operations.isSuccessfulOutcome(result)) {
+ throw new RuntimeException(String.format("Failed to resolve the expression %s: %s", value.asString(),
+ Operations.getFailureDescription(result).asString()));
+ }
+ stringValue = Operations.readResult(result).asString();
+ } else {
+ stringValue = value.asString();
+ }
+ final Matcher matcher = SIZE_PATTERN.matcher(stringValue);
+ // This shouldn't happen, but we shouldn't fail either
+ if (!matcher.matches()) {
+ // by default, rotate at 10MB
+ return 0xa0000L;
+ }
+ long qty = Long.parseLong(matcher.group(1), 10);
+ final String chr = matcher.group(2);
+ if (chr != null) {
+ switch (chr.charAt(0)) {
+ case 'b':
+ case 'B':
+ break;
+ case 'k':
+ case 'K':
+ qty <<= 10L;
+ break;
+ case 'm':
+ case 'M':
+ qty <<= 20L;
+ break;
+ case 'g':
+ case 'G':
+ qty <<= 30L;
+ break;
+ case 't':
+ case 'T':
+ qty <<= 40L;
+ break;
+ default:
+ // by default, rotate at 10MB
+ return 0xa0000L;
+ }
+ }
+ return qty;
+ }
+
+ private void parseProperties(final ModelNode model) {
+ if (model.isDefined()) {
+ for (Property property : model.asPropertyList()) {
+ final String key = property.getName();
+ if (IGNORED_PROPERTIES.contains(key)) {
+ continue;
+ }
+ final ModelNode value = property.getValue().get("value");
+ if (value.isDefined()) {
+ properties.put(key, value.asString());
+ }
+ }
+ }
+ }
+
+ private void resolveRelativeTo(final ModelNode pathModel, final String relativeTo, final StringBuilder builder) {
+ if (pathModel.hasDefined(relativeTo)) {
+ final ModelNode path = pathModel.get(relativeTo);
+ if (path.hasDefined("relative-to")) {
+ resolveRelativeTo(pathModel, path.get("relative-to").asString(), builder);
+ }
+ if (path.hasDefined("path")) {
+ final ModelNode pathEntry = path.get("path");
+ if (pathEntry.getType() == ModelType.EXPRESSION) {
+ final Collection expressions = Expression.parse(pathEntry.asExpression());
+ for (Expression expression : expressions) {
+ for (String key : expression.getKeys()) {
+ if (!properties.containsKey(key)) {
+ // @TODO, we can't use AbstractLogEnabled, it is not in the maven plugin classloader.
+ // getLogger().warn(String.format("The path %s is an undefined property. If not set at boot time
+ // unexpected results may occur.", pathEntry.asString()));
+ System.err.println(String.format(
+ "The path %s is an undefined property. If not set at boot time unexpected results may occur.",
+ pathEntry.asString()));
+ } else {
+ // We use the property name and value directly rather than referencing the path
+ usedProperties.put(key, properties.get(key));
+ expression.appendTo(builder);
+ }
+ }
+ }
+ } else {
+ if (!IGNORED_PROPERTIES.contains(relativeTo)) {
+ properties.put(relativeTo, pathEntry.asString());
+ usedProperties.put(relativeTo, pathEntry.asString());
+ }
+ builder.append("${")
+ .append(relativeTo)
+ .append("}");
+ }
+ }
+ // Use a Linux style path separator as we can't use a Windows one on Linux, but we
+ // can use a Linux one on Windows.
+ builder.append('/');
+ }
+ }
+
+ private void addUsedProperties(final Collection expressions, final String value) {
+ for (Expression expression : expressions) {
+ addUsedProperties(expression, value);
+ }
+ }
+
+ private void addUsedProperties(final Expression expression, final String value) {
+ for (String key : expression.getKeys()) {
+ usedProperties.put(key, value);
+ }
+ }
+
+ private static void writeProperty(final Writer out, final String prefix, final String name, final String value)
+ throws IOException {
+ if (name == null) {
+ writeKey(out, prefix);
+ } else {
+ writeKey(out, String.format("%s.%s", prefix, name));
+ }
+ writeValue(out, value);
+ out.write(NEW_LINE);
+ }
+
+ private static void writeValue(final Appendable out, final String value) throws IOException {
+ writeSanitized(out, value, false);
+ }
+
+ private static void writeKey(final Appendable out, final String key) throws IOException {
+ writeSanitized(out, key, true);
+ out.append('=');
+ }
+
+ private static void writeSanitized(final Appendable out, final String string, final boolean escapeSpaces)
+ throws IOException {
+ for (int x = 0; x < string.length(); x++) {
+ final char c = string.charAt(x);
+ switch (c) {
+ case ' ':
+ if (x == 0 || escapeSpaces)
+ out.append('\\');
+ out.append(c);
+ break;
+ case '\t':
+ out.append('\\').append('t');
+ break;
+ case '\n':
+ out.append('\\').append('n');
+ break;
+ case '\r':
+ out.append('\\').append('r');
+ break;
+ case '\f':
+ out.append('\\').append('f');
+ break;
+ case '\\':
+ case '=':
+ case ':':
+ case '#':
+ case '!':
+ out.append('\\').append(c);
+ break;
+ default:
+ out.append(c);
+ }
+ }
+ }
+
+ private static String modelToMap(final ModelNode value) {
+ if (value.getType() != ModelType.OBJECT) {
+ return null;
+ }
+ final List properties = value.asPropertyList();
+ final StringBuilder result = new StringBuilder();
+ final Iterator iterator = properties.iterator();
+ while (iterator.hasNext()) {
+ final Property property = iterator.next();
+ escapeKey(result, property.getName());
+ result.append('=');
+ final ModelNode v = property.getValue();
+ if (v.isDefined()) {
+ escapeValue(result, v.asString());
+ }
+ if (iterator.hasNext()) {
+ result.append(',');
+ }
+ }
+ return result.toString();
+ }
+
+ private static boolean isDefined(final ModelNode value) {
+ return value != null && value.isDefined();
+ }
+
+ private static String toCsvString(final Collection names) {
+ final StringBuilder result = new StringBuilder(1024);
+ Iterator iterator = names.iterator();
+ while (iterator.hasNext()) {
+ final String name = iterator.next();
+ // No need to write empty names
+ if (!name.isEmpty()) {
+ result.append(name);
+ if (iterator.hasNext()) {
+ result.append(",");
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ private static String resolvePropertyName(final String modelName) {
+ if ("autoflush".equals(modelName)) {
+ return "autoFlush";
+ }
+ if ("color-map".equals(modelName)) {
+ return "colors";
+ }
+ if ("syslog-format".equals(modelName)) {
+ return "syslogType";
+ }
+ if ("server-address".equals(modelName)) {
+ return "serverHostname";
+ }
+ if (modelName.contains("-")) {
+ final StringBuilder builder = new StringBuilder();
+ boolean cap = false;
+ for (char c : modelName.toCharArray()) {
+ if (c == '-') {
+ cap = true;
+ continue;
+ }
+ if (cap) {
+ builder.append(Character.toUpperCase(c));
+ cap = false;
+ } else {
+ builder.append(c);
+ }
+ }
+ return builder.toString();
+ }
+ return modelName;
+ }
+
+ /**
+ * Escapes a maps key value for serialization to a string. If the key contains a {@code \} or an {@code =} it will
+ * be escaped by a preceding {@code \}. Example: {@code key\=} or {@code \\key}.
+ *
+ * @param sb the string builder to append the escaped key to
+ * @param key the key
+ */
+ private static void escapeKey(final StringBuilder sb, final String key) {
+ final char[] chars = key.toCharArray();
+ for (int i = 0; i < chars.length; i++) {
+ final char c = chars[i];
+ // Ensure that \ and = are escaped
+ if (c == '\\') {
+ final int n = i + 1;
+ if (n >= chars.length) {
+ sb.append('\\').append('\\');
+ } else {
+ final char next = chars[n];
+ if (next == '\\' || next == '=') {
+ // Nothing to do, already properly escaped
+ sb.append(c);
+ sb.append(next);
+ i = n;
+ } else {
+ // Now we need to escape the \
+ sb.append('\\').append('\\');
+ }
+ }
+ } else if (c == '=') {
+ sb.append('\\').append(c);
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+
+ /**
+ * Escapes a maps value for serialization to a string. If a value contains a {@code \} or a {@code ,} it will be
+ * escaped by a preceding {@code \}. Example: {@code part1\,part2} or {@code value\\other}.
+ *
+ * @param sb the string builder to append the escaped value to
+ * @param value the value
+ */
+ private static void escapeValue(final StringBuilder sb, final String value) {
+ if (value != null) {
+ final char[] chars = value.toCharArray();
+ for (int i = 0; i < chars.length; i++) {
+ final char c = chars[i];
+ // Ensure that \ and , are escaped
+ if (c == '\\') {
+ final int n = i + 1;
+ if (n >= chars.length) {
+ sb.append('\\').append('\\');
+ } else {
+ final char next = chars[n];
+ if (next == '\\' || next == ',') {
+ // Nothing to do, already properly escaped
+ sb.append(c);
+ sb.append(next);
+ i = n;
+ } else {
+ // Now we need to escape the \
+ sb.append('\\').append('\\');
+ }
+ }
+ } else if (c == ',') {
+ sb.append('\\').append(c);
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/wildfly/plugins/core/bootablejar/BootableJarSupport.java b/core/src/main/java/org/wildfly/plugins/core/bootablejar/BootableJarSupport.java
new file mode 100644
index 00000000..c29f3a75
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/bootablejar/BootableJarSupport.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.wildfly.plugins.core.bootablejar;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.galleon.MessageWriter;
+import org.jboss.galleon.ProvisioningException;
+import org.jboss.galleon.api.GalleonBuilder;
+import org.jboss.galleon.api.GalleonFeaturePackRuntime;
+import org.jboss.galleon.api.GalleonProvisioningRuntime;
+import org.jboss.galleon.api.Provisioning;
+import org.jboss.galleon.api.config.GalleonProvisioningConfig;
+import org.jboss.galleon.universe.maven.MavenArtifact;
+import org.jboss.galleon.universe.maven.MavenUniverseException;
+import org.jboss.galleon.universe.maven.repo.MavenRepoManager;
+import org.jboss.galleon.util.IoUtils;
+import org.jboss.galleon.util.ZipUtils;
+import org.wildfly.plugins.core.cli.CLIForkedBootConfigGenerator;
+import org.wildfly.plugins.core.cli.ForkedCLIUtil;
+
+/**
+ *
+ * @author jdenise
+ */
+public class BootableJarSupport {
+
+ public static final String BOOTABLE_SUFFIX = "bootable";
+
+ public static final String JBOSS_MODULES_GROUP_ID = "org.jboss.modules";
+ public static final String JBOSS_MODULES_ARTIFACT_ID = "jboss-modules";
+
+ private static final String MODULE_ID_JAR_RUNTIME = "org.wildfly.bootable-jar";
+
+ private static final String BOOT_ARTIFACT_ID = "wildfly-jar-boot";
+ public static final String WILDFLY_ARTIFACT_VERSIONS_RESOURCE_PATH = "wildfly/artifact-versions.properties";
+
+ /**
+ * Package a wildfly server as a bootable JAR.
+ */
+ public static void packageBootableJar(Path targetJarFile, Path target,
+ GalleonProvisioningConfig config, Path serverHome, MavenRepoManager resolver,
+ MessageWriter writer, Log log) throws Exception {
+ Path contentRootDir = target.resolve("bootable-jar-build-artifacts");
+ if (Files.exists(contentRootDir)) {
+ IoUtils.recursiveDelete(contentRootDir);
+ }
+ Files.createDirectories(contentRootDir);
+ try {
+ ScannedArtifacts bootable;
+ Path emptyHome = contentRootDir.resolve("tmp-home");
+ Files.createDirectories(emptyHome);
+ try (Provisioning pm = new GalleonBuilder().addArtifactResolver(resolver).newProvisioningBuilder(config)
+ .setInstallationHome(emptyHome)
+ .setMessageWriter(writer)
+ .build()) {
+ bootable = scanArtifacts(pm, config, log);
+ pm.storeProvisioningConfig(config, contentRootDir.resolve("provisioning.xml"));
+ }
+ String[] paths = new String[bootable.getCliArtifacts().size()];
+ int i = 0;
+ for (MavenArtifact a : bootable.getCliArtifacts()) {
+ resolver.resolve(a);
+ paths[i] = a.getPath().toAbsolutePath().toString();
+ i += 1;
+ }
+ Path output = File.createTempFile("cli-script-output", null).toPath();
+ Files.deleteIfExists(output);
+ output.toFile().deleteOnExit();
+ IoUtils.recursiveDelete(emptyHome);
+ ForkedCLIUtil.fork(log, paths, CLIForkedBootConfigGenerator.class, serverHome, output);
+ zipServer(serverHome, contentRootDir);
+ buildJar(contentRootDir, targetJarFile, bootable, resolver);
+ } finally {
+ IoUtils.recursiveDelete(contentRootDir);
+ }
+ }
+
+ public static void unzipCloudExtension(Path contentDir, String version, MavenRepoManager resolver)
+ throws MavenUniverseException, IOException {
+ MavenArtifact ma = new MavenArtifact();
+ ma.setGroupId("org.wildfly.plugins");
+ ma.setArtifactId("wildfly-jar-cloud-extension");
+ ma.setExtension("jar");
+ ma.setVersion(version);
+ resolver.resolve(ma);
+ ZipUtils.unzip(ma.getPath(), contentDir);
+ }
+
+ public static void zipServer(Path home, Path contentDir) throws IOException {
+ cleanupServer(home);
+ Path target = contentDir.resolve("wildfly.zip");
+ ZipUtils.zip(home, target);
+ }
+
+ private static void cleanupServer(Path jbossHome) throws IOException {
+ Path history = jbossHome.resolve("standalone").resolve("configuration").resolve("standalone_xml_history");
+ IoUtils.recursiveDelete(history);
+ Files.deleteIfExists(jbossHome.resolve("README.txt"));
+ }
+
+ public static ScannedArtifacts scanArtifacts(Provisioning pm, GalleonProvisioningConfig config, Log log) throws Exception {
+ Set cliArtifacts = new HashSet<>();
+ MavenArtifact jbossModules = null;
+ MavenArtifact bootArtifact = null;
+ try (GalleonProvisioningRuntime rt = pm.getProvisioningRuntime(config)) {
+ for (GalleonFeaturePackRuntime fprt : rt.getGalleonFeaturePacks()) {
+ if (fprt.getGalleonPackage(MODULE_ID_JAR_RUNTIME) != null) {
+ // We need to discover GAV of the associated boot.
+ Path artifactProps = fprt.getResource(WILDFLY_ARTIFACT_VERSIONS_RESOURCE_PATH);
+ final Map propsMap = new HashMap<>();
+ try {
+ readProperties(artifactProps, propsMap);
+ } catch (Exception ex) {
+ throw new Exception("Error reading artifact versions", ex);
+ }
+ for (Map.Entry entry : propsMap.entrySet()) {
+ String value = entry.getValue();
+ MavenArtifact a = getArtifact(value);
+ if (BOOT_ARTIFACT_ID.equals(a.getArtifactId())) {
+ // We got it.
+ log.debug("Found " + a + " in " + fprt.getFPID());
+ bootArtifact = a;
+ break;
+ }
+ }
+ }
+ // Lookup artifacts to retrieve the required dependencies for isolated CLI execution
+ Path artifactProps = fprt.getResource(WILDFLY_ARTIFACT_VERSIONS_RESOURCE_PATH);
+ final Map propsMap = new HashMap<>();
+ try {
+ readProperties(artifactProps, propsMap);
+ } catch (Exception ex) {
+ throw new Exception("Error reading artifact versions", ex);
+ }
+ for (Map.Entry entry : propsMap.entrySet()) {
+ String value = entry.getValue();
+ MavenArtifact a = getArtifact(value);
+ if ("wildfly-cli".equals(a.getArtifactId())
+ && "org.wildfly.core".equals(a.getGroupId())) {
+ // We got it.
+ a.setClassifier("client");
+ log.debug("Found " + a + " in " + fprt.getFPID());
+ cliArtifacts.add(a);
+ continue;
+ }
+ if (JBOSS_MODULES_ARTIFACT_ID.equals(a.getArtifactId())
+ && JBOSS_MODULES_GROUP_ID.equals(a.getGroupId())) {
+ jbossModules = a;
+ }
+ }
+ }
+ }
+ if (bootArtifact == null) {
+ throw new ProvisioningException("Server doesn't support bootable jar packaging");
+ }
+ if (jbossModules == null) {
+ throw new ProvisioningException("JBoss Modules not found in dependency, can't create a Bootable JAR");
+ }
+ return new ScannedArtifacts(bootArtifact, jbossModules, cliArtifacts);
+ }
+
+ public static void buildJar(Path contentDir, Path jarFile, ScannedArtifacts bootable, MavenRepoManager resolver)
+ throws Exception {
+ resolver.resolve(bootable.getBoot());
+ Path rtJarFile = bootable.getBoot().getPath();
+ resolver.resolve(bootable.getJbossModules());
+ Path jbossModulesFile = bootable.getJbossModules().getPath();
+ ZipUtils.unzip(jbossModulesFile, contentDir);
+ ZipUtils.unzip(rtJarFile, contentDir);
+ ZipUtils.zip(contentDir, jarFile);
+ }
+
+ private static void readProperties(Path propsFile, Map propsMap) throws Exception {
+ try (BufferedReader reader = Files.newBufferedReader(propsFile)) {
+ String line = reader.readLine();
+ while (line != null) {
+ line = line.trim();
+ if (!line.isEmpty() && line.charAt(0) != '#') {
+ final int i = line.indexOf('=');
+ if (i < 0) {
+ throw new Exception("Failed to parse property " + line + " from " + propsFile);
+ }
+ propsMap.put(line.substring(0, i), line.substring(i + 1));
+ }
+ line = reader.readLine();
+ }
+ }
+ }
+
+ static MavenArtifact getArtifact(String str) {
+ final String[] parts = str.split(":");
+ final String groupId = parts[0];
+ final String artifactId = parts[1];
+ String version = parts[2];
+ String classifier = parts[3];
+ String extension = parts[4];
+
+ MavenArtifact ma = new MavenArtifact();
+ ma.setGroupId(groupId);
+ ma.setArtifactId(artifactId);
+ ma.setVersion(version);
+ ma.setClassifier(classifier);
+ ma.setExtension(extension);
+ return ma;
+ }
+}
diff --git a/core/src/main/java/org/wildfly/plugins/core/bootablejar/Expression.java b/core/src/main/java/org/wildfly/plugins/core/bootablejar/Expression.java
new file mode 100644
index 00000000..6acd5379
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/bootablejar/Expression.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.wildfly.plugins.core.bootablejar;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jboss.dmr.ValueExpression;
+
+/**
+ * A simple expression parser which only parses the possible keys and default values.
+ *
+ * @author James R. Perkins
+ */
+class Expression {
+
+ private static final int INITIAL = 0;
+ private static final int GOT_DOLLAR = 1;
+ private static final int GOT_OPEN_BRACE = 2;
+ private static final int RESOLVED = 3;
+ private static final int DEFAULT = 4;
+
+ private final List keys;
+ private final String defaultValue;
+
+ private Expression(final Collection keys, final String defaultValue) {
+ this.keys = new ArrayList<>(keys);
+ this.defaultValue = defaultValue;
+ }
+
+ /**
+ * Creates a collection of expressions based on the value.
+ *
+ * @param value the expression value
+ *
+ * @return the expression keys and default value for each expression found within the value
+ */
+ static Collection parse(final ValueExpression value) {
+ return parseExpression(value.getExpressionString());
+ }
+
+ /**
+ * Creates a collection of expressions based on the value.
+ *
+ * @param value the expression value
+ *
+ * @return the expression keys and default value for each expression found within the value
+ */
+ static Collection parse(final String expression) {
+ return parseExpression(expression);
+ }
+
+ /**
+ * All the keys associated with this expression.
+ *
+ * @return the keys
+ */
+ List getKeys() {
+ return Collections.unmodifiableList(keys);
+ }
+
+ /**
+ * Checks if there is a default value.
+ *
+ * @return {@code true} if the default value is not {@code null}, otherwise {@code false}
+ */
+ boolean hasDefault() {
+ return defaultValue != null;
+ }
+
+ /**
+ * Returns the default value which may be {@code null}.
+ *
+ * @return the default value
+ */
+ String getDefaultValue() {
+ return defaultValue;
+ }
+
+ void appendTo(final StringBuilder builder) {
+ builder.append("${");
+ final Iterator iter = keys.iterator();
+ while (iter.hasNext()) {
+ builder.append(iter.next());
+ if (iter.hasNext()) {
+ builder.append(',');
+ }
+ }
+ if (hasDefault()) {
+ builder.append(':').append(defaultValue);
+ }
+ builder.append('}');
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder();
+ appendTo(builder);
+ return builder.toString();
+ }
+
+ private static Collection parseExpression(final String expression) {
+ final Collection result = new ArrayList<>();
+ final Collection keys = new ArrayList<>();
+ final StringBuilder key = new StringBuilder();
+ String defaultValue = null;
+ final char[] chars = expression.toCharArray();
+ final int len = chars.length;
+ int state = 0;
+ int start = -1;
+ int nameStart = -1;
+ for (int i = 0; i < len; i++) {
+ char ch = chars[i];
+ switch (state) {
+ case INITIAL: {
+ if (ch == '$') {
+ state = GOT_DOLLAR;
+ }
+ continue;
+ }
+ case GOT_DOLLAR: {
+ if (ch == '{') {
+ start = i + 1;
+ nameStart = start;
+ state = GOT_OPEN_BRACE;
+ } else {
+ // invalid; emit and resume
+ state = INITIAL;
+ }
+ continue;
+ }
+ case GOT_OPEN_BRACE: {
+ switch (ch) {
+ case ':':
+ case '}':
+ case ',': {
+ final String name = expression.substring(nameStart, i).trim();
+ if ("/".equals(name)) {
+ state = ch == '}' ? INITIAL : RESOLVED;
+ continue;
+ } else if (":".equals(name)) {
+ state = ch == '}' ? INITIAL : RESOLVED;
+ continue;
+ }
+ key.append(name);
+ if (ch == '}') {
+ state = INITIAL;
+ if (key.length() > 0) {
+ keys.add(key.toString());
+ key.setLength(0);
+ }
+ result.add(new Expression(keys, defaultValue));
+ defaultValue = null;
+ keys.clear();
+ continue;
+ } else if (ch == ',') {
+ if (key.length() > 0) {
+ keys.add(key.toString());
+ key.setLength(0);
+ }
+ nameStart = i + 1;
+ continue;
+ } else {
+ start = i + 1;
+ state = DEFAULT;
+ if (key.length() > 0) {
+ keys.add(key.toString());
+ key.setLength(0);
+ }
+ continue;
+ }
+ }
+ default: {
+ continue;
+ }
+ }
+ }
+ case RESOLVED: {
+ if (ch == '}') {
+ state = INITIAL;
+ if (keys.size() > 0) {
+ result.add(new Expression(keys, defaultValue));
+ defaultValue = null;
+ keys.clear();
+ }
+ }
+ continue;
+ }
+ case DEFAULT: {
+ if (ch == '}') {
+ state = INITIAL;
+ defaultValue = expression.substring(start, i);
+ if (key.length() > 0) {
+ keys.add(key.toString());
+ key.setLength(0);
+ }
+ if (keys.size() > 0) {
+ result.add(new Expression(keys, defaultValue));
+ defaultValue = null;
+ keys.clear();
+ }
+ }
+ continue;
+ }
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ if (key.length() > 0) {
+ keys.add(key.toString());
+ result.add(new Expression(keys, defaultValue));
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/org/wildfly/plugins/core/bootablejar/Log.java b/core/src/main/java/org/wildfly/plugins/core/bootablejar/Log.java
new file mode 100644
index 00000000..e49e8adf
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/bootablejar/Log.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.wildfly.plugins.core.bootablejar;
+
+/**
+ *
+ * @author jdenise
+ */
+public interface Log {
+
+ void warn(String msg);
+
+ void debug(String msg);
+
+ void error(String msg);
+
+ void info(String msg);
+
+}
diff --git a/core/src/main/java/org/wildfly/plugins/core/bootablejar/ScannedArtifacts.java b/core/src/main/java/org/wildfly/plugins/core/bootablejar/ScannedArtifacts.java
new file mode 100644
index 00000000..8c9c048d
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/bootablejar/ScannedArtifacts.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.wildfly.plugins.core.bootablejar;
+
+import java.util.Set;
+
+import org.jboss.galleon.universe.maven.MavenArtifact;
+
+/**
+ *
+ * @author jdenise
+ */
+public class ScannedArtifacts {
+
+ private final MavenArtifact jbossModules;
+ private final MavenArtifact boot;
+ private final Set cliArtifacts;
+
+ public ScannedArtifacts(MavenArtifact bootArtifact, MavenArtifact jbossModules, Set cliArtifacts) {
+ this.boot = bootArtifact;
+ this.jbossModules = jbossModules;
+ this.cliArtifacts = cliArtifacts;
+ }
+
+ /**
+ * @return the boot
+ */
+ public MavenArtifact getBoot() {
+ return boot;
+ }
+
+ /**
+ * @return the jbossModules
+ */
+ public MavenArtifact getJbossModules() {
+ return jbossModules;
+ }
+
+ /**
+ * @return the cliArtifacts
+ */
+ public Set getCliArtifacts() {
+ return cliArtifacts;
+ }
+
+}
diff --git a/core/src/main/java/org/wildfly/plugins/core/cli/CLIForkedBootConfigGenerator.java b/core/src/main/java/org/wildfly/plugins/core/cli/CLIForkedBootConfigGenerator.java
new file mode 100644
index 00000000..abcc7300
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/cli/CLIForkedBootConfigGenerator.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.wildfly.plugins.core.cli;
+
+import java.io.FileInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Properties;
+
+import org.wildfly.plugins.core.bootablejar.BootLoggingConfiguration;
+
+/**
+ * Generate boot logging config forked process entry point.
+ *
+ * @author jdenise
+ */
+public class CLIForkedBootConfigGenerator {
+
+ public static void main(String[] args) throws Exception {
+ Path jbossHome = Paths.get(args[0]);
+ Path cliOutput = Paths.get(args[1]);
+ Path systemProperties = Paths.get(args[2]);
+ Properties properties = new Properties();
+ try (FileInputStream in = new FileInputStream(systemProperties.toFile())) {
+ properties.load(in);
+ for (String key : properties.stringPropertyNames()) {
+ System.setProperty(key, properties.getProperty(key));
+ }
+ }
+ try (CLIWrapper executor = new CLIWrapper(jbossHome, false, CLIForkedBootConfigGenerator.class.getClassLoader(),
+ new BootLoggingConfiguration())) {
+ try {
+ executor.generateBootLoggingConfig();
+ } finally {
+ Files.write(cliOutput, executor.getOutput().getBytes(StandardCharsets.UTF_8));
+ }
+ }
+ }
+}
diff --git a/plugin/src/main/java/org/wildfly/plugin/cli/CLIWrapper.java b/core/src/main/java/org/wildfly/plugins/core/cli/CLIWrapper.java
similarity index 54%
rename from plugin/src/main/java/org/wildfly/plugin/cli/CLIWrapper.java
rename to core/src/main/java/org/wildfly/plugins/core/cli/CLIWrapper.java
index 32335f75..3112ea55 100644
--- a/plugin/src/main/java/org/wildfly/plugin/cli/CLIWrapper.java
+++ b/core/src/main/java/org/wildfly/plugins/core/cli/CLIWrapper.java
@@ -2,32 +2,49 @@
* Copyright The WildFly Authors
* SPDX-License-Identifier: Apache-2.0
*/
-package org.wildfly.plugin.cli;
+package org.wildfly.plugins.core.cli;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Objects;
import org.jboss.as.controller.client.ModelControllerClient;
+import org.wildfly.plugins.core.bootablejar.BootLoggingConfiguration;
/**
- * Interacts with CLI API using reflection.
+ * A CLI executor, resolving CLI classes from the provided Classloader. We can't
+ * have cli/embedded/jboss modules in plugin classpath, it causes issue because
+ * we are sharing the same jboss module classes between execution run inside the
+ * same JVM.
+ *
+ * CLI dependencies are retrieved from provisioned server artifacts list and
+ * resolved using maven. In addition jboss-modules.jar located in the
+ * provisioned server is added.
*
* @author jdenise
*/
-class CLIWrapper implements AutoCloseable {
+public class CLIWrapper implements AutoCloseable {
private final Object ctx;
private final Method handle;
private final Method handleSafe;
private final Method terminateSession;
+ private final Method getModelControllerClient;
private final Method bindClient;
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final String origConfig;
+ private final Path jbossHome;
+ private final BootLoggingConfiguration bootLoggingConfiguration;
public CLIWrapper(Path jbossHome, boolean resolveExpression, ClassLoader loader) throws Exception {
+ this(jbossHome, resolveExpression, loader, null);
+ }
+
+ public CLIWrapper(Path jbossHome, boolean resolveExpression, ClassLoader loader,
+ BootLoggingConfiguration bootLoggingConfiguration) throws Exception {
if (jbossHome != null) {
Path config = jbossHome.resolve("bin").resolve("jboss-cli.xml");
origConfig = System.getProperty("jboss.cli.config");
@@ -37,6 +54,7 @@ public CLIWrapper(Path jbossHome, boolean resolveExpression, ClassLoader loader)
} else {
origConfig = null;
}
+ this.jbossHome = jbossHome;
final Object builder = loader.loadClass("org.jboss.as.cli.impl.CommandContextConfiguration$Builder").newInstance();
final Method setEchoCommand = builder.getClass().getMethod("setEchoCommand", boolean.class);
setEchoCommand.invoke(builder, true);
@@ -51,11 +69,9 @@ public CLIWrapper(Path jbossHome, boolean resolveExpression, ClassLoader loader)
handle = ctx.getClass().getMethod("handle", String.class);
handleSafe = ctx.getClass().getMethod("handleSafe", String.class);
terminateSession = ctx.getClass().getMethod("terminateSession");
+ getModelControllerClient = ctx.getClass().getMethod("getModelControllerClient");
bindClient = ctx.getClass().getMethod("bindClient", ModelControllerClient.class);
- }
-
- public void bindClient(ModelControllerClient client) throws Exception {
- bindClient.invoke(ctx, client);
+ this.bootLoggingConfiguration = bootLoggingConfiguration;
}
public void handle(String command) throws Exception {
@@ -66,6 +82,10 @@ public void handleSafe(String command) throws Exception {
handleSafe.invoke(ctx, command);
}
+ public void bindClient(ModelControllerClient client) throws Exception {
+ bindClient.invoke(ctx, client);
+ }
+
public String getOutput() {
return out.toString();
}
@@ -81,4 +101,37 @@ public void close() throws Exception {
}
}
+ private ModelControllerClient getModelControllerClient() throws Exception {
+ return (ModelControllerClient) getModelControllerClient.invoke(ctx);
+ }
+
+ public void generateBootLoggingConfig() throws Exception {
+ Objects.requireNonNull(bootLoggingConfiguration);
+ Exception toThrow = null;
+ try {
+ // Start the embedded server
+ handle("embed-server --jboss-home=" + jbossHome + " --std-out=discard");
+ // Get the client used to execute the management operations
+ final ModelControllerClient client = getModelControllerClient();
+ // Update the bootable logging config
+ final Path configDir = jbossHome.resolve("standalone").resolve("configuration");
+ bootLoggingConfiguration.generate(configDir, client);
+ } catch (Exception e) {
+ toThrow = e;
+ } finally {
+ try {
+ // Always stop the embedded server
+ handle("stop-embedded-server");
+ } catch (Exception e) {
+ if (toThrow != null) {
+ e.addSuppressed(toThrow);
+ }
+ toThrow = e;
+ }
+ }
+ // Check if an error has been thrown and throw it.
+ if (toThrow != null) {
+ throw toThrow;
+ }
+ }
}
diff --git a/core/src/main/java/org/wildfly/plugins/core/cli/ForkedCLIUtil.java b/core/src/main/java/org/wildfly/plugins/core/cli/ForkedCLIUtil.java
new file mode 100644
index 00000000..f5eb9788
--- /dev/null
+++ b/core/src/main/java/org/wildfly/plugins/core/cli/ForkedCLIUtil.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.wildfly.plugins.core.cli;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URISyntaxException;
+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.util.ArrayList;
+import java.util.List;
+
+import org.jboss.galleon.BaseErrors;
+import org.jboss.galleon.ProvisioningException;
+import org.wildfly.plugins.core.bootablejar.Log;
+
+/**
+ *
+ * @author jdenise
+ */
+public class ForkedCLIUtil {
+
+ private static String javaHome;
+ private static String javaCmd;
+
+ private static String getJavaHome() {
+ return javaHome == null ? javaHome = System.getProperty("java.home") : javaHome;
+ }
+
+ private static String getJavaCmd() {
+ return javaCmd == null ? javaCmd = Paths.get(getJavaHome()).resolve("bin").resolve("java").toString() : javaCmd;
+ }
+
+ public static void fork(Log log, String[] artifacts, Class> clazz, Path home, Path output, String... args)
+ throws Exception {
+ // prepare the classpath
+ final StringBuilder cp = new StringBuilder();
+ for (String loc : artifacts) {
+ cp.append(loc).append(File.pathSeparator);
+ }
+ StringBuilder contextCP = new StringBuilder();
+ collectCpUrls(getJavaHome(), Thread.currentThread().getContextClassLoader(), contextCP);
+ // This happens when running tests, use the process classpath to retrieve the CLIForkedExecutor main class
+ if (contextCP.length() == 0) {
+ log.debug("Re-using process classpath to retrieve Maven plugin classes to fork CLI process.");
+ cp.append(System.getProperty("java.class.path"));
+ } else {
+ cp.append(contextCP);
+ }
+ Path properties = storeSystemProps();
+
+ final List argsList = new ArrayList<>();
+ argsList.add(getJavaCmd());
+ argsList.add("-server");
+ argsList.add("-cp");
+ argsList.add(cp.toString());
+ argsList.add(clazz.getName());
+ argsList.add(home.toString());
+ argsList.add(output.toString());
+ argsList.add(properties.toString());
+ for (String s : args) {
+ argsList.add(s);
+ }
+ log.debug("CLI process command line " + argsList);
+ try {
+ final Process p;
+ try {
+ p = new ProcessBuilder(argsList).redirectErrorStream(true).start();
+ } catch (IOException e) {
+ throw new ProvisioningException("Failed to start forked process", e);
+ }
+ StringBuilder traces = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
+ String line = reader.readLine();
+ while (line != null) {
+ traces.append(line).append(System.lineSeparator());
+ line = reader.readLine();
+ }
+ if (p.isAlive()) {
+ try {
+ p.waitFor();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ int exitCode = p.exitValue();
+ if (exitCode != 0) {
+ log.error("Error executing CLI:" + traces);
+ throw new Exception("CLI execution failed:" + traces);
+ }
+ } finally {
+ Files.deleteIfExists(properties);
+ }
+ }
+
+ private static Path storeSystemProps() throws ProvisioningException {
+ final Path props;
+ try {
+ props = Files.createTempFile("wfbootablejar", "sysprops");
+ } catch (IOException e) {
+ throw new ProvisioningException("Failed to create a tmp file", e);
+ }
+ try (BufferedWriter writer = Files.newBufferedWriter(props)) {
+ System.getProperties().store(writer, "");
+ } catch (IOException e) {
+ throw new ProvisioningException(BaseErrors.writeFile(props), e);
+ }
+ return props;
+ }
+
+ private static void collectCpUrls(String javaHome, ClassLoader cl, StringBuilder buf) throws URISyntaxException {
+ final ClassLoader parentCl = cl.getParent();
+ if (parentCl != null) {
+ collectCpUrls(javaHome, cl.getParent(), buf);
+ }
+ if (cl instanceof URLClassLoader) {
+ for (URL url : ((URLClassLoader) cl).getURLs()) {
+ final String file = new File(url.toURI()).getAbsolutePath();
+ if (file.startsWith(javaHome)) {
+ continue;
+ }
+ if (buf.length() > 0) {
+ buf.append(File.pathSeparatorChar);
+ }
+ buf.append(file);
+ }
+ }
+ }
+}
diff --git a/core/src/test/java/org/wildfly/plugin/core/Environment.java b/core/src/test/java/org/wildfly/plugin/core/Environment.java
index 2a4a04da..b0d9fccf 100644
--- a/core/src/test/java/org/wildfly/plugin/core/Environment.java
+++ b/core/src/test/java/org/wildfly/plugin/core/Environment.java
@@ -8,6 +8,8 @@
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.Collections;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.logging.Logger;
@@ -16,7 +18,7 @@
* @author James R. Perkins
*/
@SuppressWarnings({ "WeakerAccess", "Duplicates" })
-class Environment {
+public class Environment {
/**
* The default WildFly home directory specified by the {@code jboss.home} system property.
@@ -37,6 +39,9 @@ class Environment {
*/
public static final long TIMEOUT;
+ private static final String TMP_DIR = System.getProperty("java.io.tmpdir", "target");
+ private static final int LOG_SERVER_PORT = getProperty("ts.log.server.port", 10514);
+ private static final Collection JVM_ARGS;
static {
final Logger logger = Logger.getLogger(Environment.class);
@@ -62,6 +67,12 @@ class Environment {
logger.debugf(e, "Invalid timeout: %s", timeout);
throw new RuntimeException("Invalid timeout: " + timeout, e);
}
+ final String jvmArgs = System.getProperty("test.jvm.args");
+ if (jvmArgs == null) {
+ JVM_ARGS = Collections.emptyList();
+ } else {
+ JVM_ARGS = Utils.splitArguments(jvmArgs);
+ }
}
public static ModelControllerClient createClient() throws UnknownHostException {
@@ -73,4 +84,43 @@ private static void validateWildFlyHome(final Path wildflyHome) {
throw new RuntimeException("Invalid WildFly home directory: " + wildflyHome);
}
}
+
+ /**
+ * Creates a temporary path based on the {@code java.io.tmpdir} system
+ * property.
+ *
+ * @param paths the additional portions of the path
+ *
+ * @return the path
+ */
+ public static Path createTempPath(final String... paths) {
+ return Paths.get(TMP_DIR, paths);
+ }
+
+ /**
+ * Gets the log server port
+ *
+ * The default is 10514 and can be overridden via the
+ * {@code ts.log.server.port} system property.
+ *
+ *
+ * @return the log server port
+ */
+ public static int getLogServerPort() {
+ return LOG_SERVER_PORT;
+ }
+
+ /**
+ * Returns a collection of the JVM arguments to set for any server started during the test process.
+ *
+ * @return the JVM arguments
+ */
+ public static Collection getJvmArgs() {
+ return JVM_ARGS;
+ }
+
+ private static int getProperty(final String name, final int dft) {
+ final String value = System.getProperty(name);
+ return value == null ? dft : Integer.parseInt(value);
+ }
}
diff --git a/core/src/test/java/org/wildfly/plugins/core/bootablejar/BootLoggingConfigurationIT.java b/core/src/test/java/org/wildfly/plugins/core/bootablejar/BootLoggingConfigurationIT.java
new file mode 100644
index 00000000..44277f78
--- /dev/null
+++ b/core/src/test/java/org/wildfly/plugins/core/bootablejar/BootLoggingConfigurationIT.java
@@ -0,0 +1,869 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ *
+ * Copyright 2020 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed 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.wildfly.plugins.core.bootablejar;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+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.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Pattern;
+
+import org.jboss.as.controller.client.ModelControllerClient;
+import org.jboss.as.controller.client.Operation;
+import org.jboss.as.controller.client.helpers.ClientConstants;
+import org.jboss.as.controller.client.helpers.Operations;
+import org.jboss.as.controller.client.helpers.Operations.CompositeOperationBuilder;
+import org.jboss.dmr.ModelNode;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.wildfly.core.launcher.Launcher;
+import org.wildfly.core.launcher.StandaloneCommandBuilder;
+import org.wildfly.plugin.core.Environment;
+import org.wildfly.plugin.core.ServerHelper;
+
+/**
+ * @author James R. Perkins
+ */
+public class BootLoggingConfigurationIT {
+
+ private static final Pattern EXPRESSION_PATTERN = Pattern.compile(".*\\$\\{.*}.*");
+ private static Process currentProcess;
+ private static Path stdout;
+ private static ModelControllerClient client;
+
+ @Rule
+ public TestName testName = new TestName();
+
+ private final Deque tearDownOps = new ArrayDeque<>();
+ private Path tmpDir;
+
+ @BeforeClass
+ public static void startWildFly() throws Exception {
+ stdout = Files.createTempFile("stdout-", ".log");
+ final StandaloneCommandBuilder builder = StandaloneCommandBuilder.of(Environment.WILDFLY_HOME)
+ .addJavaOptions(Environment.getJvmArgs());
+ currentProcess = Launcher.of(builder)
+ .setRedirectErrorStream(true)
+ .redirectOutput(stdout)
+ .launch();
+ client = ModelControllerClient.Factory.create(Environment.HOSTNAME, Environment.PORT);
+ // Wait for standalone to start
+ ServerHelper.waitForStandalone(currentProcess, client, Environment.TIMEOUT);
+ Assert.assertTrue(String.format("Standalone server is not running:%n%s", getLog()),
+ ServerHelper.isStandaloneRunning(client));
+ }
+
+ @AfterClass
+ public static void shutdown() throws Exception {
+ if (client != null) {
+ ServerHelper.shutdownStandalone(client);
+ client.close();
+ }
+ if (currentProcess != null) {
+ if (!currentProcess.waitFor(Environment.TIMEOUT, TimeUnit.SECONDS)) {
+ currentProcess.destroyForcibly();
+ }
+ }
+ }
+
+ @Before
+ public void setup() throws Exception {
+ tmpDir = Environment.createTempPath("test-config", testName.getMethodName());
+ if (Files.notExists(tmpDir)) {
+ Files.createDirectories(tmpDir);
+ }
+ }
+
+ @After
+ public void cleanUp() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+ ModelNode op;
+ while ((op = tearDownOps.pollFirst()) != null) {
+ builder.addStep(op);
+ }
+ executeOperation(builder.build());
+ }
+
+ @Test
+ public void testDefault() throws Exception {
+ generateAndTest();
+ }
+
+ @Test
+ public void testAsyncHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Add a file handler
+ final ModelNode fileHandler = createLoggingAddress("file-handler", "test-file");
+ ModelNode op = Operations.createAddOperation(fileHandler);
+ op.get("named-formatter").set("PATTERN");
+ op.get("append").set(true);
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("jboss.server.log.dir");
+ file.get("path").set("test-file.log");
+ builder.addStep(op);
+
+ // Add the async handler
+ final ModelNode asyncAddress = createLoggingAddress("async-handler", "async");
+ op = Operations.createAddOperation(asyncAddress);
+ op.get("overflow-action").set("DISCARD");
+ op.get("queue-length").set(5000);
+ final ModelNode subhandlers = op.get("subhandlers").setEmptyList();
+ subhandlers.add("test-file");
+ builder.addStep(op);
+
+ // Add the handler to the root-logger
+ builder.addStep(createAddHandlerOp("async"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(asyncAddress));
+ tearDownOps.add(Operations.createRemoveOperation(fileHandler));
+ generateAndTest();
+ }
+
+ @Test
+ public void testDefaultConsole() throws Exception {
+ final ModelNode address = createLoggingAddress("console-handler", "new-handler");
+ // Just do a raw add which will add the default formatter rather than a named-formatter
+ executeOperation(Operations.createAddOperation(address));
+ tearDownOps.add(Operations.createRemoveOperation(address));
+ generateAndTest();
+ }
+
+ @Test
+ public void testCustomHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ final ModelNode formatterAddress = createLoggingAddress("custom-formatter", "json");
+ ModelNode op = Operations.createAddOperation(formatterAddress);
+ op.get("class").set("org.jboss.logmanager.formatters.JsonFormatter");
+ op.get("module").set("org.jboss.logmanager");
+ ModelNode properties = op.get("properties");
+ properties.get("prettyPrint").set("true");
+ properties.get("recordDelimiter").set("|");
+ builder.addStep(op);
+
+ final ModelNode handlerAddress = createLoggingAddress("custom-handler", "custom-console");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("class").set("org.jboss.logmanager.handlers.ConsoleHandler");
+ op.get("module").set("org.jboss.logmanager");
+ op.get("named-formatter").set("json");
+ properties = op.get("properties");
+ properties.get("target").set("SYSTEM_ERR");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("custom-console"));
+
+ executeOperation(builder.build());
+ // Create the tear down ops
+ tearDownOps.addLast(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.addLast(Operations.createRemoveOperation(formatterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testCustomHandlerNoProperties() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ final ModelNode formatterAddress = createLoggingAddress("custom-formatter", "json");
+ ModelNode op = Operations.createAddOperation(formatterAddress);
+ op.get("class").set("org.jboss.logmanager.formatters.JsonFormatter");
+ op.get("module").set("org.jboss.logmanager");
+ builder.addStep(op);
+
+ final ModelNode handlerAddress = createLoggingAddress("custom-handler", "custom-console");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("class").set("org.jboss.logmanager.handlers.ConsoleHandler");
+ op.get("module").set("org.jboss.logmanager");
+ op.get("named-formatter").set("json");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("custom-console"));
+
+ executeOperation(builder.build());
+ // Create the tear down ops
+ tearDownOps.addLast(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.addLast(Operations.createRemoveOperation(formatterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testPeriodicRotatingFileHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Create a handler to assign the formatter to
+ final ModelNode handlerAddress = createLoggingAddress("periodic-rotating-file-handler", "new-file");
+ final ModelNode op = Operations.createAddOperation(handlerAddress);
+ op.get("named-formatter").set("PATTERN");
+ op.get("suffix").set(".yyyy-MM-dd");
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("jboss.server.log.dir");
+ file.get("path").set("test.log");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("new-file"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testPeriodicSizeRotatingFileHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Create a handler to assign the formatter to
+ final ModelNode handlerAddress = createLoggingAddress("periodic-size-rotating-file-handler", "new-file");
+ final ModelNode op = Operations.createAddOperation(handlerAddress);
+ op.get("named-formatter").set("PATTERN");
+ op.get("suffix").set(".yyyy-MM-dd");
+ op.get("rotate-on-boot").set(false);
+ op.get("rotate-size").set("${test.rotate.size:50M}");
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("jboss.server.log.dir");
+ file.get("path").set("test.log");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("new-file"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testSizeRotatingFileHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Create a handler to assign the formatter to
+ final ModelNode handlerAddress = createLoggingAddress("size-rotating-file-handler", "new-file");
+ final ModelNode op = Operations.createAddOperation(handlerAddress);
+ op.get("named-formatter").set("PATTERN");
+ op.get("rotate-on-boot").set(false);
+ op.get("rotate-size").set("50M");
+ op.get("max-backup-index").set(100);
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("jboss.server.log.dir");
+ file.get("path").set("test.log");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("new-file"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ @Ignore("This test is failing on CI. See WFCORE-5155.")
+ public void testSocketHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Add the socket binding
+ final ModelNode socketBindingAddress = Operations.createAddress("socket-binding-group", "standard-sockets",
+ "remote-destination-outbound-socket-binding", "log-server");
+ ModelNode op = Operations.createAddOperation(socketBindingAddress);
+ op.get("host").set(Environment.HOSTNAME);
+ op.get("port").set(Environment.getLogServerPort());
+ builder.addStep(op);
+
+ // Add a socket handler
+ final ModelNode address = createLoggingAddress("socket-handler", "socket");
+ op = Operations.createAddOperation(address);
+ op.get("named-formatter").set("PATTERN");
+ op.get("outbound-socket-binding-ref").set("log-server");
+ builder.addStep(op);
+
+ // Add the handler to the root-logger
+ builder.addStep(createAddHandlerOp("socket"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(address));
+ tearDownOps.add(Operations.createRemoveOperation(socketBindingAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testSyslogHandler() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Add a socket handler
+ final ModelNode address = createLoggingAddress("syslog-handler", "syslog");
+ final ModelNode op = Operations.createAddOperation(address);
+ op.get("app-name").set("test-app");
+ op.get("enabled").set(false);
+ op.get("facility").set("local-use-0");
+ op.get("hostname").set(Environment.HOSTNAME);
+ op.get("level").set("WARN");
+ op.get("named-formatter").set("PATTERN");
+ op.get("port").set(Environment.getLogServerPort());
+ builder.addStep(op);
+
+ // Add the handler to the root-logger
+ builder.addStep(createAddHandlerOp("syslog"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(address));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testFilter() throws Exception {
+ final ModelNode filterAddress = createLoggingAddress("filter", "testFilter");
+ ModelNode op = Operations.createAddOperation(filterAddress);
+ op.get("class").set(TestFilter.class.getName());
+ op.get("module").set("org.wildfly.plugins.core.bootablejar");
+ final ModelNode constructorProperties = op.get("constructor-properties");
+ constructorProperties.get("constructorText").set(" | constructor property text");
+ final ModelNode properties = op.get("properties");
+ properties.get("propertyText").set(" | property text");
+ executeOperation(op);
+ tearDownOps.add(Operations.createRemoveOperation(filterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testFilterNoProperties() throws Exception {
+ final ModelNode filterAddress = createLoggingAddress("filter", "testFilter");
+ ModelNode op = Operations.createAddOperation(filterAddress);
+ op.get("class").set(TestFilter.class.getName());
+ op.get("module").set("org.wildfly.plugins.core.bootablejar");
+ executeOperation(op);
+ tearDownOps.add(Operations.createRemoveOperation(filterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testJsonFormatter() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ final ModelNode formatterAddress = createLoggingAddress("json-formatter", "json");
+ ModelNode op = Operations.createAddOperation(formatterAddress);
+ op.get("pretty-print").set(false);
+ op.get("exception-output-type").set("${test.type:formatted}");
+ op.get("date-format").set("yyyy-MM-dd'T'HH:mm:SSSZ");
+
+ final ModelNode keyOverrides = op.get("key-overrides").setEmptyObject();
+ keyOverrides.get("message").set("msg");
+ keyOverrides.get("stack-trace").set("cause");
+
+ final ModelNode metaData = op.get("meta-data").setEmptyObject();
+ metaData.get("app-name").set("test");
+ metaData.get("@version").set("1");
+
+ op.get("print-details").set(true);
+ op.get("record-delimiter").set("\n");
+ op.get("zone-id").set("GMT");
+ builder.addStep(op);
+
+ // Create a handler to assign the formatter to
+ final ModelNode handlerAddress = createLoggingAddress("file-handler", "json-file");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("append").set(false);
+ op.get("level").set("DEBUG");
+ op.get("named-formatter").set("json");
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("jboss.server.log.dir");
+ file.get("path").set("test-json.log");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("json-file"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(formatterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testPatternFormatter() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ final ModelNode formatterAddress = createLoggingAddress("pattern-formatter", "new-pattern");
+ ModelNode op = Operations.createAddOperation(formatterAddress);
+ op.get("pattern").set("[test] %d{HH:mm:ss,SSS} %-5p [%c] %s%e%n");
+ op.get("color-map").set("info:blue,warn:yellow,error:red,debug:cyan");
+ builder.addStep(op);
+
+ // Create a handler to assign the formatter to
+ final ModelNode handlerAddress = createLoggingAddress("file-handler", "new-file");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("append").set(false);
+ op.get("encoding").set("ISO-8859-1");
+ op.get("level").set("DEBUG");
+ op.get("filter-spec").set("any(accept,match(\".*\"))");
+ op.get("named-formatter").set("new-pattern");
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("jboss.server.log.dir");
+ file.get("path").set("test.log");
+ builder.addStep(op);
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(formatterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testLogger() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Add a filter for the logger
+ final ModelNode filterAddress = createLoggingAddress("filter", "testFilter");
+ ModelNode op = Operations.createAddOperation(filterAddress);
+ op.get("class").set(TestFilter.class.getName());
+ op.get("module").set("org.wildfly.plugins.core.bootablejar");
+ builder.addStep(op);
+
+ // Add a formatter for the handler
+ final ModelNode formatterAddress = createLoggingAddress("pattern-formatter", "custom-formatter");
+ op = Operations.createAddOperation(formatterAddress);
+ op.get("pattern").set("[%X{debug.token} %K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n");
+ builder.addStep(op);
+
+ // Add a handler for the logger
+ final ModelNode handlerAddress = createLoggingAddress("console-handler", "custom-console");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("named-formatter").set("custom-formatter");
+ builder.addStep(op);
+
+ // Create the logger
+ final ModelNode loggerAddress = createLoggingAddress("logger", "org.jboss.as");
+ op = Operations.createAddOperation(loggerAddress);
+ op.get("level").set("${test.level:DEBUG}");
+ op.get("use-parent-handlers").set(false);
+ op.get("filter-spec").set("all(testFilter)");
+ final ModelNode handlers = op.get("handlers").setEmptyList();
+ handlers.add("custom-console");
+ builder.addStep(op);
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(loggerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(formatterAddress));
+ tearDownOps.add(Operations.createRemoveOperation(filterAddress));
+
+ generateAndTest();
+ }
+
+ @Test
+ public void testWithProperties() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Create some expected properties
+ final Properties expectedProperties = new Properties();
+ expectedProperties.setProperty("test.level", "TRACE");
+ expectedProperties.setProperty("test.rotate-on-boot", "true");
+ expectedProperties.setProperty("test.pretty.print", "true");
+ expectedProperties.setProperty("test.exception-output-type", "formatted");
+ expectedProperties.setProperty("test.zone.id", "UTC");
+ expectedProperties.setProperty("test.dir", System.getProperty("java.io.tmpdir"));
+
+ // Add the system properties
+ for (String key : expectedProperties.stringPropertyNames()) {
+ final ModelNode address = Operations.createAddress("system-property", key);
+ final ModelNode op = Operations.createAddOperation(address);
+ op.get("value").set(expectedProperties.getProperty(key));
+ builder.addStep(op);
+ }
+ // Add a path and set this after
+ final ModelNode tmpPathAddress = Operations.createAddress("path", "custom.log.dir");
+ ModelNode op = Operations.createAddOperation(tmpPathAddress);
+ op.get("path").set("${test.dir}");
+ builder.addStep(op);
+
+ final ModelNode logPathAddress = Operations.createAddress("path", "test.log.dir");
+ op = Operations.createAddOperation(logPathAddress);
+ op.get("relative-to").set("custom.log.dir");
+ op.get("path").set("logs");
+ builder.addStep(op);
+
+ // Add one property that won't be used so it shouldn't end up in the boot-config.properties
+ final ModelNode sysPropAddress = Operations.createAddress("system-property", "unused.property");
+ op = Operations.createAddOperation(sysPropAddress);
+ op.get("value").set("not used");
+ builder.addStep(op);
+ tearDownOps.add(Operations.createRemoveOperation(sysPropAddress));
+
+ // Create a formatter
+ final ModelNode formatterAddress = createLoggingAddress("json-formatter", "json");
+ op = Operations.createAddOperation(formatterAddress);
+ op.get("pretty-print").set("${test.pretty.print:false}");
+ op.get("exception-output-type").set("${test.exception-output-type:detailed}");
+ op.get("zone-id").set("${test.zone.id:GMT}");
+ builder.addStep(op);
+
+ // Create a file handler
+ final ModelNode handlerAddress = createLoggingAddress("size-rotating-file-handler", "json-file");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("named-formatter").set("json");
+ op.get("rotate-on-boot").set("${test.rotate-on-boot:false}");
+ op.get("rotate-size").set("50M");
+ op.get("max-backup-index").set(100);
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("test.log.dir");
+ file.get("path").set("test.log");
+ builder.addStep(op);
+ // We don't actually expect the custom.log.dir property here as it should be written to the file as
+ // ${test.dir}/${test.log.dir}/test.log
+ expectedProperties.setProperty("test.log.dir", "logs");
+
+ // Create a logger
+ final ModelNode loggerAddress = createLoggingAddress("logger", "org.wildfly.core");
+ op = Operations.createAddOperation(loggerAddress);
+ op.get("level").set("${test.level:INFO}");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("json-file"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(loggerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(formatterAddress));
+ tearDownOps.add(Operations.createRemoveOperation(logPathAddress));
+ tearDownOps.add(Operations.createRemoveOperation(tmpPathAddress));
+
+ // Remove all the properties last
+ for (String name : expectedProperties.stringPropertyNames()) {
+ // test.log.dir isn't an actual system property
+ if ("test.log.dir".equals(name))
+ continue;
+ final ModelNode address = Operations.createAddress("system-property", name);
+ tearDownOps.addLast(Operations.createRemoveOperation(address));
+ }
+
+ generateAndTest(expectedProperties);
+ }
+
+ @Test
+ public void testNestedPaths() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Create some expected properties
+ final Properties expectedProperties = new Properties();
+ // Add a path and set this after
+ final ModelNode tmpPathAddress = Operations.createAddress("path", "custom.log.dir");
+ ModelNode op = Operations.createAddOperation(tmpPathAddress);
+ op.get("path").set("custom-logs");
+ op.get("relative-to").set("jboss.server.log.dir");
+ builder.addStep(op);
+
+ final ModelNode logPathAddress = Operations.createAddress("path", "test.log.dir");
+ op = Operations.createAddOperation(logPathAddress);
+ op.get("relative-to").set("custom.log.dir");
+ op.get("path").set("logs");
+ builder.addStep(op);
+ expectedProperties.setProperty("custom.log.dir", "custom-logs");
+ expectedProperties.setProperty("test.log.dir", "logs");
+
+ // Create a file handler
+ final ModelNode handlerAddress = createLoggingAddress("file-handler", "test-file");
+ op = Operations.createAddOperation(handlerAddress);
+ op.get("named-formatter").set("PATTERN");
+ final ModelNode file = op.get("file");
+ file.get("relative-to").set("test.log.dir");
+ file.get("path").set("test.log");
+ builder.addStep(op);
+
+ builder.addStep(createAddHandlerOp("test-file"));
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(handlerAddress));
+ tearDownOps.add(Operations.createRemoveOperation(logPathAddress));
+ tearDownOps.add(Operations.createRemoveOperation(tmpPathAddress));
+
+ generateAndTest(expectedProperties);
+ }
+
+ @Test
+ public void testMultiKeyExpression() throws Exception {
+ final CompositeOperationBuilder builder = CompositeOperationBuilder.create();
+
+ // Create some expected properties
+ final Properties expectedProperties = new Properties();
+ expectedProperties.setProperty("test.prod.level", "INFO");
+ expectedProperties.setProperty("test.min.level", "WARN");
+
+ // Add the system properties
+ for (String key : expectedProperties.stringPropertyNames()) {
+ final ModelNode address = Operations.createAddress("system-property", key);
+ final ModelNode op = Operations.createAddOperation(address);
+ op.get("value").set(expectedProperties.getProperty(key));
+ builder.addStep(op);
+ tearDownOps.add(Operations.createRemoveOperation(address));
+ }
+
+ // Create a logger to set the level on
+ final ModelNode address = createLoggingAddress("logger", BootLoggingConfigurationIT.class.getName());
+ final ModelNode op = Operations.createAddOperation(address);
+ op.get("level").set("${test.dev.level,test.prod.level,test.min.level:DEBUG}");
+ builder.addStep(op);
+
+ executeOperation(builder.build());
+ tearDownOps.add(Operations.createRemoveOperation(address));
+
+ generateAndTest(expectedProperties);
+ }
+
+ private void generateAndTest() throws Exception {
+ generateAndTest(null);
+ }
+
+ private void generateAndTest(final Properties expectedBootConfig) throws Exception {
+ final BootLoggingConfiguration config = new BootLoggingConfiguration();
+ // @TODO, we can't use AbstractLogEnabled, it is not in the maven plugin classloader.
+ // config.enableLogging(TestLogger.getLogger(BootLoggingConfigurationTestCase.class));
+ config.generate(tmpDir, client);
+ compare(load(findLoggingConfig(), true, true),
+ load(tmpDir.resolve("logging.properties"), false, true), true);
+ final Path bootConfig = tmpDir.resolve("boot-config.properties");
+ if (expectedBootConfig == null) {
+ // The file should not exist
+ Assert.assertTrue("Expected " + bootConfig + " not to exist", Files.notExists(bootConfig));
+ } else {
+ compare(expectedBootConfig, load(bootConfig, false, false), false);
+ }
+ }
+
+ private ModelNode createAddHandlerOp(final String handlerName) {
+ final ModelNode address = createLoggingAddress("root-logger", "ROOT");
+ // Create the remove op first
+ ModelNode op = Operations.createOperation("remove-handler", address);
+ op.get("name").set(handlerName);
+ tearDownOps.addFirst(op);
+
+ // Create the add op
+ op = Operations.createOperation("add-handler", address);
+ op.get("name").set(handlerName);
+ return op;
+ }
+
+ private Path findLoggingConfig() throws IOException {
+ final Path serverLogConfig = Environment.WILDFLY_HOME.resolve("standalone").resolve("configuration")
+ .resolve("logging.properties");
+ Assert.assertTrue("Could find config file " + serverLogConfig, Files.exists(serverLogConfig));
+ return Files.copy(serverLogConfig, tmpDir.resolve("server-logging.properties"), StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ private static ModelNode createLoggingAddress(final String... parts) {
+ final Collection addresses = new ArrayList<>();
+ addresses.add("subsystem");
+ addresses.add("logging");
+ Collections.addAll(addresses, parts);
+ return Operations.createAddress(addresses);
+ }
+
+ private static ModelNode executeOperation(final ModelNode op) throws IOException {
+ return executeOperation(Operation.Factory.create(op));
+ }
+
+ private static ModelNode executeOperation(final Operation op) throws IOException {
+ final ModelNode result = client.execute(op);
+ if (!Operations.isSuccessfulOutcome(result)) {
+ Assert.fail(String.format("Operation %s failed: %s", op.getOperation(),
+ Operations.getFailureDescription(result).asString()));
+ }
+ // Reload if required
+ if (result.hasDefined(ClientConstants.RESPONSE_HEADERS)) {
+ final ModelNode responseHeaders = result.get(ClientConstants.RESPONSE_HEADERS);
+ if (responseHeaders.hasDefined("process-state")) {
+ if (ClientConstants.CONTROLLER_PROCESS_STATE_RELOAD_REQUIRED
+ .equals(responseHeaders.get("process-state").asString())) {
+ executeOperation(Operations.createOperation("reload"));
+ try {
+ ServerHelper.waitForStandalone(currentProcess, client, Environment.TIMEOUT);
+ } catch (InterruptedException | TimeoutException e) {
+ e.printStackTrace();
+ Assert.fail("Reloading the server failed: " + e.getLocalizedMessage());
+ }
+ }
+ }
+ }
+ return Operations.readResult(result);
+ }
+
+ private static String getLog() throws IOException {
+ final StringBuilder result = new StringBuilder();
+ Files.readAllLines(stdout, StandardCharsets.UTF_8).forEach(line -> result.append(line).append(System.lineSeparator()));
+ return result.toString();
+ }
+
+ private static void compare(final Properties expected, final Properties found, final boolean resolveExpressions)
+ throws IOException {
+ compareKeys(expected, found);
+ compareValues(expected, found, resolveExpressions);
+ }
+
+ private static void compareKeys(final Properties expected, final Properties found) {
+ final Set expectedKeys = new TreeSet<>(expected.stringPropertyNames());
+ final Set foundKeys = new TreeSet<>(found.stringPropertyNames());
+ // Find the missing expected keys
+ final Set missing = new TreeSet<>(expectedKeys);
+ missing.removeAll(foundKeys);
+ Assert.assertTrue("Missing the following keys in the generated file: " + missing.toString(),
+ missing.isEmpty());
+
+ // Find additional keys
+ missing.addAll(foundKeys);
+ missing.removeAll(expectedKeys);
+ Assert.assertTrue("Found the following extra keys in the generated file: " + missing.toString(),
+ missing.isEmpty());
+ }
+
+ private static void compareValues(final Properties expected, final Properties found, final boolean resolveExpressions)
+ throws IOException {
+ final Set keys = new TreeSet<>(expected.stringPropertyNames());
+ for (String key : keys) {
+ final String expectedValue = expected.getProperty(key);
+ final String foundValue = found.getProperty(key);
+ if (key.endsWith("fileName")) {
+ final Path foundFileName = resolvePath(foundValue);
+ Assert.assertEquals(Paths.get(expectedValue).normalize(), foundFileName);
+ } else {
+ if (expectedValue.contains(",")) {
+ // Assume the values are a list
+ final List expectedValues = stringToList(expectedValue);
+ final List foundValues = stringToList(foundValue);
+ Assert.assertEquals(String.format("Found %s expected %s", foundValues, expectedValues), expectedValues,
+ foundValues);
+ } else {
+ if (resolveExpressions && EXPRESSION_PATTERN.matcher(foundValue).matches()) {
+ String resolvedValue = resolveExpression(foundValue);
+ // Handle some special cases
+ if ("formatted".equals(resolvedValue)) {
+ resolvedValue = resolvedValue.toUpperCase();
+ }
+ Assert.assertEquals(expectedValue, resolvedValue);
+ } else {
+ Assert.assertEquals(expectedValue, foundValue);
+ }
+ }
+ }
+ }
+ }
+
+ private static List stringToList(final String value) {
+ final List result = new ArrayList<>();
+ Collections.addAll(result, value.split(","));
+ Collections.sort(result);
+ return result;
+ }
+
+ private static Properties load(final Path path, final boolean expected, final boolean filter) throws IOException {
+ final Properties result = new Properties();
+ try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
+ result.load(reader);
+ }
+ if (filter) {
+ if (expected) {
+ result.remove("handlers");
+ result.remove("formatters");
+ result.remove("filters");
+ } else {
+ // For some reason the default console-handler and periodic-rotating-file-handler don't persist the enabled
+ // attribute.
+ for (String key : result.stringPropertyNames()) {
+ if (key.equals("handler.CONSOLE.enabled") || key.equals("handler.FILE.enabled")) {
+ result.remove(key);
+ final String propertiesKey = resolvePrefix(key) + ".properties";
+ final String value = result.getProperty(propertiesKey);
+ if (value != null) {
+ if ("enabled".equals(value)) {
+ result.remove(propertiesKey);
+ } else {
+ result.setProperty(propertiesKey, value.replace("enabled,", "").replace(",enabled", ""));
+ }
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ private static String resolvePrefix(final String key) {
+ final int i = key.lastIndexOf('.');
+ if (i > 0) {
+ return key.substring(0, i);
+ }
+ return key;
+ }
+
+ private static Path resolvePath(final String path) throws IOException {
+ Path resolved = Paths.get(path);
+ if (EXPRESSION_PATTERN.matcher(path).matches()) {
+ // For testing purposes we're just going to use the last entry which should be a path entry
+ final LinkedList expressions = new LinkedList<>(Expression.parse(path));
+ Assert.assertFalse("The path could not be resolved: " + path, expressions.isEmpty());
+ final Expression expression = expressions.getLast();
+ // We're assuming we only have one key entry which for testing purposes should be okay
+ final ModelNode op = Operations.createOperation("path-info",
+ Operations.createAddress("path", expression.getKeys().get(0)));
+ final ModelNode result = client.execute(op);
+ if (!Operations.isSuccessfulOutcome(result)) {
+ Assert.fail(Operations.getFailureDescription(result).asString());
+ }
+ final ModelNode pathInfo = Operations.readResult(result);
+ final String resolvedPath = pathInfo.get("path", "resolved-path").asString();
+ resolved = Paths.get(resolvedPath, resolved.getFileName().toString());
+ }
+ return resolved.normalize();
+ }
+
+ private static String resolveExpression(final String value) throws IOException {
+ // Resolve the expression
+ ModelNode op = Operations.createOperation("resolve-expression");
+ op.get("expression").set(value);
+ return executeOperation(op).asString();
+ }
+}
diff --git a/core/src/test/java/org/wildfly/plugins/core/bootablejar/TestFilter.java b/core/src/test/java/org/wildfly/plugins/core/bootablejar/TestFilter.java
new file mode 100644
index 00000000..333b4960
--- /dev/null
+++ b/core/src/test/java/org/wildfly/plugins/core/bootablejar/TestFilter.java
@@ -0,0 +1,86 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ *
+ * Copyright 2019 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed 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.wildfly.plugins.core.bootablejar;
+
+import java.util.logging.Filter;
+import java.util.logging.LogRecord;
+
+/**
+ * @author James R. Perkins
+ */
+@SuppressWarnings({ "WeakerAccess", "unused" })
+public class TestFilter implements Filter {
+ private final String constructorText;
+ private final boolean isLoggable;
+ private String propertyText;
+
+ public TestFilter() {
+ this(null, true);
+ }
+
+ public TestFilter(final boolean isLoggable) {
+ this(null, isLoggable);
+ }
+
+ public TestFilter(final String constructorText) {
+ this(constructorText, true);
+ }
+
+ public TestFilter(final String constructorText, final boolean isLoggable) {
+ this.constructorText = constructorText;
+ this.isLoggable = isLoggable;
+ }
+
+ @Override
+ public boolean isLoggable(final LogRecord record) {
+ if (isLoggable) {
+ final StringBuilder newMsg = new StringBuilder(record.getMessage());
+ if (constructorText != null) {
+ newMsg.append(constructorText);
+ }
+ if (propertyText != null) {
+ newMsg.append(propertyText);
+ }
+ record.setMessage(newMsg.toString());
+ }
+ return isLoggable;
+ }
+
+ public String getPropertyText() {
+ return propertyText;
+ }
+
+ public void setPropertyText(final String propertyText) {
+ this.propertyText = propertyText;
+ }
+
+ public String getConstructorText() {
+ return constructorText;
+ }
+
+ @Override
+ public String toString() {
+ return TestFilter.class.getName() +
+ "[constructorText=" + constructorText +
+ ", isLoggable=" + isLoggable +
+ ", propertyText=" + propertyText +
+ "]";
+ }
+}
diff --git a/core/src/test/modules/org/wildfly/plugins/core/bootablejar/main/module.xml b/core/src/test/modules/org/wildfly/plugins/core/bootablejar/main/module.xml
new file mode 100644
index 00000000..1f4827bc
--- /dev/null
+++ b/core/src/test/modules/org/wildfly/plugins/core/bootablejar/main/module.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugin/src/main/java/org/wildfly/plugin/cli/LocalCLIExecutor.java b/plugin/src/main/java/org/wildfly/plugin/cli/LocalCLIExecutor.java
index ca11c711..331485e3 100644
--- a/plugin/src/main/java/org/wildfly/plugin/cli/LocalCLIExecutor.java
+++ b/plugin/src/main/java/org/wildfly/plugin/cli/LocalCLIExecutor.java
@@ -31,6 +31,7 @@
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.galleon.universe.maven.MavenArtifact;
import org.jboss.galleon.universe.maven.repo.MavenRepoManager;
+import org.wildfly.plugins.core.cli.CLIWrapper;
/**
* A CLI executor, resolving CLI artifact from Maven.
diff --git a/plugin/src/main/java/org/wildfly/plugin/common/PropertyNames.java b/plugin/src/main/java/org/wildfly/plugin/common/PropertyNames.java
index ff9618da..1e94176d 100644
--- a/plugin/src/main/java/org/wildfly/plugin/common/PropertyNames.java
+++ b/plugin/src/main/java/org/wildfly/plugin/common/PropertyNames.java
@@ -16,6 +16,12 @@ public interface PropertyNames {
String BATCH = "wildfly.batch";
+ String BOOTABLE_JAR = "wildfly.bootable.jar";
+
+ String BOOTABLE_JAR_INSTALL_CLASSIFIER = "wildfly.bootable.jar.classifier";
+
+ String BOOTABLE_JAR_NAME = "wildfly.bootable.jar.name";
+
String CHANNELS = "wildfly.channels";
String CHECK_PACKAGING = "wildfly.checkPackaging";
diff --git a/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java b/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java
index 57ee6ca1..ddff6b51 100644
--- a/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java
+++ b/plugin/src/main/java/org/wildfly/plugin/dev/DevMojo.java
@@ -96,7 +96,7 @@
import org.wildfly.plugin.server.VersionComparator;
/**
- * Starts a standalone instance of WildFly and deploys the application to the server. The deployment type myst be a WAR.
+ * Starts a standalone instance of WildFly and deploys the application to the server. The deployment type must be a WAR.
* Once the server is running, the source directories are monitored for changes. If required the sources will be compiled
* and the deployment may be redeployed.
*
@@ -105,6 +105,10 @@
* terminated and restarted.
*
*
+ *
+ * Note that if a WildFly Bootable JAR is packaged, it is ignored by this goal.
+ *
+ *
* @author James R. Perkins
* @since 4.1
*/
diff --git a/plugin/src/main/java/org/wildfly/plugin/provision/ApplicationImageMojo.java b/plugin/src/main/java/org/wildfly/plugin/provision/ApplicationImageMojo.java
index 58457ec9..baa16f09 100644
--- a/plugin/src/main/java/org/wildfly/plugin/provision/ApplicationImageMojo.java
+++ b/plugin/src/main/java/org/wildfly/plugin/provision/ApplicationImageMojo.java
@@ -31,6 +31,10 @@
*
* The {@code image} goal relies on a Docker binary to execute all image commands (build, login, push).
*
+ *
+ * Note that if a WildFly Bootable JAR is packaged, it is ignored when building the image.
+ *
+ *
* @since 4.0
*/
@Mojo(name = "image", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
diff --git a/plugin/src/main/java/org/wildfly/plugin/provision/PackageServerMojo.java b/plugin/src/main/java/org/wildfly/plugin/provision/PackageServerMojo.java
index 51f4bf70..71df8495 100644
--- a/plugin/src/main/java/org/wildfly/plugin/provision/PackageServerMojo.java
+++ b/plugin/src/main/java/org/wildfly/plugin/provision/PackageServerMojo.java
@@ -30,6 +30,7 @@
import org.jboss.galleon.ProvisioningException;
import org.jboss.galleon.api.GalleonBuilder;
import org.jboss.galleon.api.config.GalleonProvisioningConfig;
+import org.jboss.galleon.maven.plugin.util.MvnMessageWriter;
import org.jboss.galleon.util.IoUtils;
import org.wildfly.glow.ScanResults;
import org.wildfly.plugin.cli.BaseCommandConfiguration;
@@ -40,6 +41,8 @@
import org.wildfly.plugin.common.Utils;
import org.wildfly.plugin.deployment.MojoDeploymentException;
import org.wildfly.plugin.deployment.PackageType;
+import org.wildfly.plugins.core.bootablejar.BootableJarSupport;
+import org.wildfly.plugins.core.bootablejar.Log;
/**
* Provision a server, copy extra content and deploy primary artifact if it
@@ -51,6 +54,9 @@
@Mojo(name = "package", requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, defaultPhase = LifecyclePhase.PACKAGE)
public class PackageServerMojo extends AbstractProvisionServerMojo {
+ private static final String JAR = "jar";
+ private static final String BOOTABLE_JAR_NAME_RADICAL = "server-";
+
/**
* A list of directories to copy content to the provisioned server. If a
* directory is not absolute, it has to be relative to the project base
@@ -212,9 +218,35 @@ public class PackageServerMojo extends AbstractProvisionServerMojo {
@Parameter(alias = "discover-provisioning-info")
private GlowConfig discoverProvisioningInfo;
+ /**
+ * Package the provisioned server into a WildFly Bootable JAR.
+ *
+ * Note that the produced fat JAR is ignored when running the {@code dev},{@code image},{@code start} or {@code run} goals.
+ *
+ */
+ @Parameter(alias = "bootable-jar", required = false, property = PropertyNames.BOOTABLE_JAR)
+ private boolean bootableJar;
+
+ /**
+ * When {@code bootable-jar} is set to true, use this parameter to name the generated jar file.
+ * The jar file is named by default {@code server-bootable.jar}.
+ */
+ @Parameter(alias = "bootable-jar-name", required = false, property = PropertyNames.BOOTABLE_JAR_NAME)
+ private String bootableJarName;
+
+ /**
+ * When {@code bootable-jar} is set to true, the bootable JAR artifact is attached to the project with the classifier
+ * 'bootable'. Use this parameter to
+ * configure the classifier.
+ */
+ @Parameter(alias = "bootable-jar-install-artifact-classifier", property = PropertyNames.BOOTABLE_JAR_INSTALL_CLASSIFIER, defaultValue = BootableJarSupport.BOOTABLE_SUFFIX)
+ private String bootableJarInstallArtifactClassifier;
+
@Inject
private OfflineCommandExecutor commandExecutor;
+ private GalleonProvisioningConfig config;
+
@Override
protected GalleonProvisioningConfig getDefaultConfig() throws ProvisioningException {
return null;
@@ -224,7 +256,8 @@ protected GalleonProvisioningConfig getDefaultConfig() throws ProvisioningExcept
protected GalleonProvisioningConfig buildGalleonConfig(GalleonBuilder pm)
throws MojoExecutionException, ProvisioningException {
if (discoverProvisioningInfo == null) {
- return super.buildGalleonConfig(pm);
+ config = super.buildGalleonConfig(pm);
+ return config;
}
try {
try (ScanResults results = Utils.scanDeployment(discoverProvisioningInfo,
@@ -239,7 +272,8 @@ protected GalleonProvisioningConfig buildGalleonConfig(GalleonBuilder pm)
pm,
galleonOptions,
layersConfigurationFileName)) {
- return results.getProvisioningConfig();
+ config = results.getProvisioningConfig();
+ return config;
}
} catch (Exception ex) {
throw new MojoExecutionException(ex.getLocalizedMessage(), ex);
@@ -321,11 +355,60 @@ protected void serverProvisioned(Path jbossHome) throws MojoExecutionException,
}
cleanupServer(jbossHome);
- } catch (IOException ex) {
+ if (bootableJar) {
+ packageBootableJar(jbossHome, config);
+ }
+ } catch (Exception ex) {
throw new MojoExecutionException(ex.getLocalizedMessage(), ex);
}
}
+ private void attachJar(Path jarFile) {
+ if (getLog().isDebugEnabled()) {
+ getLog().debug("Attaching bootable jar " + jarFile + " as a project artifact with classifier "
+ + bootableJarInstallArtifactClassifier);
+ }
+ projectHelper.attachArtifact(project, JAR, bootableJarInstallArtifactClassifier, jarFile.toFile());
+ }
+
+ private void packageBootableJar(Path jbossHome, GalleonProvisioningConfig activeConfig) throws Exception {
+ String jarName = bootableJarName == null ? BOOTABLE_JAR_NAME_RADICAL + BootableJarSupport.BOOTABLE_SUFFIX + "." + JAR
+ : bootableJarName;
+ Path targetPath = Paths.get(project.getBuild().getDirectory());
+ Path targetJarFile = targetPath.toAbsolutePath()
+ .resolve(jarName);
+ Files.deleteIfExists(targetJarFile);
+ BootableJarSupport.packageBootableJar(targetJarFile, targetPath,
+ activeConfig, jbossHome,
+ artifactResolver,
+ new MvnMessageWriter(getLog()), new Log() {
+ @Override
+ public void warn(String string) {
+ getLog().warn(string);
+ }
+
+ @Override
+ public void debug(String string) {
+ if (getLog().isDebugEnabled()) {
+ getLog().debug(string);
+ }
+ }
+
+ @Override
+ public void error(String string) {
+ getLog().error(string);
+ }
+
+ @Override
+ public void info(String string) {
+ getLog().info(string);
+ }
+ });
+ attachJar(targetJarFile);
+ getLog().info("Bootable JAR packaging DONE. To run the server: java -jar " + targetJarFile);
+
+ }
+
/**
* Return the file name of the deployment to put in the server deployment directory
*
diff --git a/plugin/src/main/java/org/wildfly/plugin/server/RunMojo.java b/plugin/src/main/java/org/wildfly/plugin/server/RunMojo.java
index 17f2bfb5..95adbe01 100644
--- a/plugin/src/main/java/org/wildfly/plugin/server/RunMojo.java
+++ b/plugin/src/main/java/org/wildfly/plugin/server/RunMojo.java
@@ -36,6 +36,10 @@
*
* This goal will block until cancelled or a shutdown is invoked from a management client.
*
+ *
+ * Note that if a WildFly Bootable JAR is packaged, it is ignored by this goal.
+ *
+ *
* @author Stuart Douglas
* @author James R. Perkins
*/
diff --git a/plugin/src/main/java/org/wildfly/plugin/server/StartMojo.java b/plugin/src/main/java/org/wildfly/plugin/server/StartMojo.java
index d3f92d5e..3a5de43d 100644
--- a/plugin/src/main/java/org/wildfly/plugin/server/StartMojo.java
+++ b/plugin/src/main/java/org/wildfly/plugin/server/StartMojo.java
@@ -23,6 +23,10 @@
*
* The purpose of this goal is to start a WildFly Application Server for testing during the maven lifecycle.
*
+ *
+ * Note that if a WildFly Bootable JAR is packaged, it is ignored by this goal.
+ *
+ *
* @author James R. Perkins
*/
@Mojo(name = "start", requiresDependencyResolution = ResolutionScope.RUNTIME)
diff --git a/tests/bootable-tests/pom.xml b/tests/bootable-tests/pom.xml
new file mode 100644
index 00000000..a3ce5ced
--- /dev/null
+++ b/tests/bootable-tests/pom.xml
@@ -0,0 +1,118 @@
+
+
+
+
+ 4.0.0
+
+ org.wildfly.plugins
+ wildfly-maven-plugin-tests
+ 5.0.0.Alpha3-SNAPSHOT
+
+
+ wildfly-maven-plugin-bootable-tests
+ WildFly Maven Plugin - Bootable Tests
+
+ Tests the maven plugin goals with a bootable JAR
+
+
+
+ ${project.build.testOutputDirectory}${file.separator}test-project
+
+
+
+
+
+ org.wildfly.plugins
+ wildfly-maven-plugin-tests-shared
+ ${project.version}
+ test
+
+
+ junit
+ junit
+ test
+
+
+ org.apache.maven.resolver
+ maven-resolver-api
+ test
+
+
+ org.apache.maven.resolver
+ maven-resolver-spi
+ test
+
+
+ org.apache.maven.resolver
+ maven-resolver-impl
+ test
+
+
+ org.apache.maven.resolver
+ maven-resolver-connector-basic
+ test
+
+
+ org.apache.maven.resolver
+ maven-resolver-transport-wagon
+ test
+
+
+
+ org.apache.maven.wagon
+ wagon-http
+ test
+
+
+ org.apache.maven.resolver
+ maven-resolver-util
+ test
+
+
+
+ org.jboss.slf4j
+ slf4j-jboss-logging
+
+
+
+
+
+
+ org.jboss.galleon
+ galleon-maven-plugin
+
+
+ provision-wildfly
+ none
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+ maven-surefire-plugin
+
+
+ true
+
+
+
+
+
+
diff --git a/tests/bootable-tests/src/test/java/org/wildfly/plugin/bootable/PackageBootableTest.java b/tests/bootable-tests/src/test/java/org/wildfly/plugin/bootable/PackageBootableTest.java
new file mode 100644
index 00000000..fa35bb13
--- /dev/null
+++ b/tests/bootable-tests/src/test/java/org/wildfly/plugin/bootable/PackageBootableTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright The WildFly Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.wildfly.plugin.bootable;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.maven.plugin.Mojo;
+import org.junit.Test;
+import org.wildfly.plugin.tests.AbstractProvisionConfiguredMojoTestCase;
+import org.wildfly.plugin.tests.AbstractWildFlyMojoTest;
+
+public class PackageBootableTest extends AbstractProvisionConfiguredMojoTestCase {
+
+ private static final String BOOTABLE_JAR_NAME = "server-bootable.jar";
+
+ public PackageBootableTest() {
+ super("wildfly-maven-plugin");
+ }
+
+ @Test
+ public void testBootablePackage() throws Exception {
+
+ final Mojo packageMojo = lookupConfiguredMojo(
+ AbstractWildFlyMojoTest.getPomFile("package-bootable-pom.xml").toFile(), "package");
+ packageMojo.execute();
+ String[] layers = { "jaxrs-server" };
+ String deploymentName = "test.war";
+ checkJar(AbstractWildFlyMojoTest.getBaseDir(), BOOTABLE_JAR_NAME, deploymentName,
+ true, layers, null, true);
+ checkDeployment(AbstractWildFlyMojoTest.getBaseDir(), BOOTABLE_JAR_NAME, "test");
+ }
+
+ @Test
+ public void testBootableRootPackage() throws Exception {
+
+ final Mojo packageMojo = lookupConfiguredMojo(
+ AbstractWildFlyMojoTest.getPomFile("package-bootable-root-pom.xml").toFile(), "package");
+ String deploymentName = "ROOT.war";
+ Path rootWar = AbstractWildFlyMojoTest.getBaseDir().resolve("target").resolve(deploymentName);
+ Path testWar = AbstractWildFlyMojoTest.getBaseDir().resolve("target").resolve("test.war");
+ Files.copy(testWar, rootWar);
+ Files.delete(testWar);
+ packageMojo.execute();
+ String[] layers = { "jaxrs-server" };
+ String fileName = "jar-root.jar";
+ checkJar(AbstractWildFlyMojoTest.getBaseDir(), fileName, deploymentName, true, layers, null, true);
+ checkDeployment(AbstractWildFlyMojoTest.getBaseDir(), fileName, null);
+ }
+
+ @Test
+ public void testGlowPackage() throws Exception {
+
+ final Mojo packageMojo = lookupConfiguredMojo(
+ AbstractWildFlyMojoTest.getPomFile("package-bootable-glow-pom.xml").toFile(), "package");
+ String[] layers = { "ee-core-profile-server", "microprofile-openapi" };
+ packageMojo.execute();
+ String deploymentName = "test.war";
+ checkJar(AbstractWildFlyMojoTest.getBaseDir(), BOOTABLE_JAR_NAME, deploymentName,
+ true, layers, null, true);
+ }
+}
diff --git a/tests/bootable-tests/src/test/resources/test-project/.gitignore b/tests/bootable-tests/src/test/resources/test-project/.gitignore
new file mode 100644
index 00000000..60cda34a
--- /dev/null
+++ b/tests/bootable-tests/src/test/resources/test-project/.gitignore
@@ -0,0 +1,2 @@
+# Include the test target directory
+!target/
\ No newline at end of file
diff --git a/tests/bootable-tests/src/test/resources/test-project/package-bootable-glow-pom.xml b/tests/bootable-tests/src/test/resources/test-project/package-bootable-glow-pom.xml
new file mode 100644
index 00000000..ea4b56fb
--- /dev/null
+++ b/tests/bootable-tests/src/test/resources/test-project/package-bootable-glow-pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ 4.0.0
+ testing
+ testing
+ 0.1.0-SNAPSHOT
+
+
+
+
+ org.wildfly.plugins
+ wildfly-maven-plugin
+
+ test.war
+ true
+ packaged-bootable-glow-server
+ true
+
+
+ openapi
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/bootable-tests/src/test/resources/test-project/package-bootable-pom.xml b/tests/bootable-tests/src/test/resources/test-project/package-bootable-pom.xml
new file mode 100644
index 00000000..c3ed9b24
--- /dev/null
+++ b/tests/bootable-tests/src/test/resources/test-project/package-bootable-pom.xml
@@ -0,0 +1,50 @@
+
+
+
+ 4.0.0
+ testing
+ testing
+ 0.1.0-SNAPSHOT
+
+
+
+
+ org.wildfly.plugins
+ wildfly-maven-plugin
+
+
+
+ wildfly@maven(org.jboss.universe:community-universe)#WF_VERSION
+
+
+
+ jaxrs-server
+
+ test.war
+ true
+ packaged-bootable-server
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/bootable-tests/src/test/resources/test-project/package-bootable-root-pom.xml b/tests/bootable-tests/src/test/resources/test-project/package-bootable-root-pom.xml
new file mode 100644
index 00000000..17f5e1c2
--- /dev/null
+++ b/tests/bootable-tests/src/test/resources/test-project/package-bootable-root-pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ 4.0.0
+ testing
+ testing
+ 0.1.0-SNAPSHOT
+
+
+
+
+ org.wildfly.plugins
+ wildfly-maven-plugin
+
+
+
+ wildfly@maven(org.jboss.universe:community-universe)#WF_VERSION
+
+
+
+ jaxrs-server
+
+ ROOT.war
+ true
+ packaged-root-bootable-server
+ true
+ jar-root.jar
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/bootable-tests/src/test/resources/test-project/target/test.war b/tests/bootable-tests/src/test/resources/test-project/target/test.war
new file mode 100644
index 00000000..5f037b93
Binary files /dev/null and b/tests/bootable-tests/src/test/resources/test-project/target/test.war differ
diff --git a/tests/pom.xml b/tests/pom.xml
index c2d2689d..a883e6a9 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -18,6 +18,7 @@
pom
+ bootable-tests
standalone-tests
domain-tests
shared
diff --git a/tests/shared/src/main/java/org/wildfly/plugin/tests/AbstractProvisionConfiguredMojoTestCase.java b/tests/shared/src/main/java/org/wildfly/plugin/tests/AbstractProvisionConfiguredMojoTestCase.java
index 01ebfd1f..856d86d9 100644
--- a/tests/shared/src/main/java/org/wildfly/plugin/tests/AbstractProvisionConfiguredMojoTestCase.java
+++ b/tests/shared/src/main/java/org/wildfly/plugin/tests/AbstractProvisionConfiguredMojoTestCase.java
@@ -15,7 +15,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
import org.apache.maven.DefaultMaven;
import org.apache.maven.Maven;
import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
@@ -35,15 +40,25 @@
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory;
import org.eclipse.aether.repository.LocalRepository;
+import org.jboss.as.controller.client.ModelControllerClient;
+import org.jboss.galleon.api.GalleonBuilder;
+import org.jboss.galleon.api.Provisioning;
+import org.jboss.galleon.api.config.GalleonConfigurationWithLayers;
+import org.jboss.galleon.api.config.GalleonProvisioningConfig;
import org.jboss.galleon.config.ConfigId;
import org.jboss.galleon.config.ConfigModel;
import org.jboss.galleon.config.ProvisioningConfig;
+import org.jboss.galleon.util.IoUtils;
import org.jboss.galleon.util.PathsUtils;
+import org.jboss.galleon.util.ZipUtils;
import org.jboss.galleon.xml.ProvisioningXmlParser;
import org.junit.Assert;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.wildfly.core.launcher.ProcessHelper;
+import org.wildfly.plugin.core.ServerHelper;
+import org.wildfly.plugins.core.bootablejar.BootableJarSupport;
/**
* A class to construct a properly configured MOJO.
@@ -229,4 +244,188 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr
assertEquals(Files.exists(wildflyHome.resolve(".galleon")), stateRecorded);
assertEquals(Files.exists(wildflyHome.resolve(".wildfly-maven-plugin-provisioning.xml")), !stateRecorded);
}
+
+ protected void checkJar(Path dir, String fileName, String deploymentName, boolean expectDeployment,
+ String[] layers, String[] excludedLayers, boolean stateRecorded, String... configTokens) throws Exception {
+ Path wildflyHome = null;
+ try {
+ wildflyHome = checkAndGetWildFlyHome(dir, fileName, deploymentName, expectDeployment, layers,
+ excludedLayers,
+ stateRecorded,
+ configTokens);
+ } finally {
+ if (wildflyHome != null) {
+ IoUtils.recursiveDelete(wildflyHome);
+ }
+ }
+ }
+
+ protected Path checkAndGetWildFlyHome(Path dir, String fileName, String deploymentName, boolean expectDeployment,
+ String[] layers, String[] excludedLayers, boolean stateRecorded, String... configTokens) throws Exception {
+ Path tmpDir = Files.createTempDirectory("bootable-jar-test-unzipped");
+ Path wildflyHome = Files.createTempDirectory("bootable-jar-test-unzipped-" + BootableJarSupport.BOOTABLE_SUFFIX);
+ try {
+ Path jar = dir.resolve("target")
+ .resolve(fileName == null ? "server-" + BootableJarSupport.BOOTABLE_SUFFIX + ".jar" : fileName);
+ assertTrue(Files.exists(jar));
+
+ ZipUtils.unzip(jar, tmpDir);
+ Path zippedWildfly = tmpDir.resolve("wildfly.zip");
+ assertTrue(Files.exists(zippedWildfly));
+
+ Path provisioningFile = tmpDir.resolve("provisioning.xml");
+ assertTrue(Files.exists(provisioningFile));
+
+ ZipUtils.unzip(zippedWildfly, wildflyHome);
+ if (expectDeployment) {
+ assertTrue(Files.exists(wildflyHome.resolve("standalone/deployments").resolve(deploymentName)));
+ } else {
+ assertFalse(Files.exists(wildflyHome.resolve("standalone/deployments").resolve(deploymentName)));
+ }
+ Path history = wildflyHome.resolve("standalone").resolve("configuration").resolve("standalone_xml_history");
+ assertFalse(Files.exists(history));
+
+ Path configFile = wildflyHome.resolve("standalone/configuration/standalone.xml");
+ assertTrue(Files.exists(configFile));
+ if (layers != null) {
+ Path pFile = PathsUtils.getProvisioningXml(wildflyHome);
+ assertTrue(Files.exists(pFile));
+ try (Provisioning provisioning = new GalleonBuilder().newProvisioningBuilder(pFile).build()) {
+ GalleonProvisioningConfig configDescription = provisioning.loadProvisioningConfig(pFile);
+ GalleonConfigurationWithLayers config = null;
+ for (GalleonConfigurationWithLayers c : configDescription.getDefinedConfigs()) {
+ if (c.getModel().equals("standalone") && c.getName().equals("standalone.xml")) {
+ config = c;
+ }
+ }
+ assertNotNull(config);
+ assertEquals(layers.length, config.getIncludedLayers().size());
+ for (String layer : layers) {
+ assertTrue(config.getIncludedLayers().contains(layer));
+ }
+ if (excludedLayers != null) {
+ for (String layer : excludedLayers) {
+ assertTrue(config.getExcludedLayers().contains(layer));
+ }
+ }
+ }
+ }
+ if (configTokens != null) {
+ String str = new String(Files.readAllBytes(configFile), StandardCharsets.UTF_8);
+ for (String token : configTokens) {
+ assertTrue(str, str.contains(token));
+ }
+ }
+ } finally {
+ IoUtils.recursiveDelete(tmpDir);
+ }
+ assertEquals(Files.exists(wildflyHome.resolve(".galleon")), stateRecorded);
+ return wildflyHome;
+ }
+
+ protected void checkDeployment(Path dir, String fileName, String deploymentName) throws Exception {
+ checkURL(dir, fileName, createUrl(TestEnvironment.HTTP_PORT, (deploymentName == null ? "" : deploymentName)), true);
+ }
+
+ protected static String createUrl(final int port, final String... paths) {
+ final StringBuilder result = new StringBuilder(32)
+ .append("http://")
+ .append(TestEnvironment.HOSTNAME)
+ .append(':')
+ .append(port);
+ for (String path : paths) {
+ result.append('/')
+ .append(path);
+ }
+ return result.toString();
+ }
+
+ protected boolean checkURL(String url) {
+ try {
+ try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
+ HttpGet httpget = new HttpGet(url);
+
+ CloseableHttpResponse response = httpclient.execute(httpget);
+ System.out.println("STATUS CODE " + response.getStatusLine().getStatusCode());
+ return response.getStatusLine().getStatusCode() == 200;
+ }
+ } catch (Exception ex) {
+ System.out.println(ex);
+ return false;
+ }
+ }
+
+ private static void shutdown() throws IOException {
+ try (ModelControllerClient client = ModelControllerClient.Factory.create(TestEnvironment.HOSTNAME,
+ TestEnvironment.PORT)) {
+ if (ServerHelper.isStandaloneRunning(client)) {
+ ServerHelper.shutdownStandalone(client, (int) TestEnvironment.TIMEOUT);
+ }
+ }
+ }
+
+ protected void checkURL(Path dir, String fileName, String url, boolean start, String... args) throws Exception {
+ Process process = null;
+ int timeout = (int) TestEnvironment.TIMEOUT * 1000;
+ long sleep = 1000;
+ boolean success = false;
+ try {
+ if (start) {
+ process = startServer(dir, fileName, args);
+ }
+ // Check the server state in all cases. All test cases are provisioning the manager layer.
+ try (ModelControllerClient client = ModelControllerClient.Factory.create(TestEnvironment.HOSTNAME,
+ TestEnvironment.PORT)) {
+ // Wait for the server to start, this calls into the management interface.
+ ServerHelper.waitForStandalone(process, client, TestEnvironment.TIMEOUT);
+ }
+
+ if (url == null) {
+ // Checking for the server state is enough.
+ success = true;
+ } else {
+ while (timeout > 0) {
+ if (checkURL(url)) {
+ System.out.println("Successfully connected to " + url);
+ success = true;
+ break;
+ }
+ Thread.sleep(sleep);
+ timeout -= sleep;
+ }
+ }
+ if (process != null) {
+ assertTrue(process.isAlive());
+ }
+ shutdown();
+ // If the process is not null wait for it to shutdown
+ if (process != null) {
+ assertTrue("The process has failed to shutdown", process.waitFor(TestEnvironment.TIMEOUT, TimeUnit.SECONDS));
+ }
+ } finally {
+ ProcessHelper.destroyProcess(process);
+ }
+ if (!success) {
+ throw new Exception("Unable to interact with deployed application");
+ }
+ }
+
+ protected Process startServer(Path dir, String fileName, String... args) throws Exception {
+ List cmd = new ArrayList<>();
+ cmd.add(TestEnvironment.getJavaCommand(null));
+ cmd.add("-jar");
+ cmd.add(dir.resolve("target").resolve(fileName).toAbsolutePath().toString());
+ cmd.add("-Djboss.management.http.port=" + TestEnvironment.PORT);
+ cmd.add("-Djboss.http.port=" + TestEnvironment.HTTP_PORT);
+ cmd.addAll(Arrays.asList(args));
+ final Path out = Files.createTempFile("logs-package-bootable", "-process.txt");
+ final Path parent = out.getParent();
+ if (parent != null && Files.notExists(parent)) {
+ Files.createDirectories(parent);
+ }
+ return new ProcessBuilder(cmd)
+ .redirectErrorStream(true)
+ .redirectOutput(out.toFile())
+ .start();
+ }
}
diff --git a/tests/shared/src/main/java/org/wildfly/plugin/tests/TestEnvironment.java b/tests/shared/src/main/java/org/wildfly/plugin/tests/TestEnvironment.java
index 5adc960f..e080b10b 100644
--- a/tests/shared/src/main/java/org/wildfly/plugin/tests/TestEnvironment.java
+++ b/tests/shared/src/main/java/org/wildfly/plugin/tests/TestEnvironment.java
@@ -37,6 +37,11 @@ public class TestEnvironment extends Environment {
*/
public static final int PORT;
+ /**
+ * The port specified by the {@code wildfly.http.port} system property or {@code 8880} by default.
+ */
+ public static final int HTTP_PORT;
+
/**
* The default server startup timeout specified by {@code wildfly.timeout}, default is 60 seconds.
*/
@@ -45,12 +50,14 @@ public class TestEnvironment extends Environment {
static {
final Logger logger = Logger.getLogger(TestEnvironment.class);
-
- // Get the WildFly home directory and copy to the temp directory
- final String wildflyDist = System.getProperty("jboss.home");
- assert wildflyDist != null : "WildFly home property, jboss.home, was not set";
- Path wildflyHome = Paths.get(wildflyDist);
- validateWildFlyHome(wildflyHome);
+ Path wildflyHome = null;
+ if (!Boolean.getBoolean("wildfly.test.bootable")) {
+ // Get the WildFly home directory and copy to the temp directory
+ final String wildflyDist = System.getProperty("jboss.home");
+ assert wildflyDist != null : "WildFly home property, jboss.home, was not set";
+ wildflyHome = Paths.get(wildflyDist);
+ validateWildFlyHome(wildflyHome);
+ }
WILDFLY_HOME = wildflyHome;
final String port = System.getProperty("wildfly.management.port", "9990");
@@ -60,6 +67,13 @@ public class TestEnvironment extends Environment {
logger.debugf(e, "Invalid port: %s", port);
throw new RuntimeException("Invalid port: " + port, e);
}
+ final String httpPort = System.getProperty("wildfly.http.port", "8080");
+ try {
+ HTTP_PORT = Integer.parseInt(httpPort);
+ } catch (NumberFormatException e) {
+ logger.debugf(e, "Invalid port: %s", port);
+ throw new RuntimeException("Invalid port: " + port, e);
+ }
final String timeout = System.getProperty("wildfly.timeout", "60");
try {