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 {