diff --git a/src/main/java/org/jboss/jws/diag/Main.java b/src/main/java/org/jboss/jws/diag/Main.java index 811e8b1..5467f26 100644 --- a/src/main/java/org/jboss/jws/diag/Main.java +++ b/src/main/java/org/jboss/jws/diag/Main.java @@ -4,6 +4,7 @@ import org.jboss.jws.diag.config.ConfigCommand; import org.jboss.jws.diag.diff.DiffCommand; import org.jboss.jws.diag.logs.LogsCommand; +import org.jboss.jws.diag.modcluster.ModClusterCommand; import org.jboss.jws.diag.summary.SummaryCommand; import org.jboss.jws.diag.validate.ValidateCommand; import picocli.CommandLine; @@ -19,6 +20,7 @@ ValidateCommand.class, BundleCommand.class, LogsCommand.class, + ModClusterCommand.class, DiffCommand.class } ) diff --git a/src/main/java/org/jboss/jws/diag/modcluster/ModClusterCommand.java b/src/main/java/org/jboss/jws/diag/modcluster/ModClusterCommand.java new file mode 100644 index 0000000..b8d10bb --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/modcluster/ModClusterCommand.java @@ -0,0 +1,85 @@ +package org.jboss.jws.diag.modcluster; + +import org.jboss.jws.diag.common.ExitCodes; +import org.jboss.jws.diag.common.OutputFormat; +import org.jboss.jws.diag.common.OutputFormatMixin; +import org.jboss.jws.diag.modcluster.formatter.ModClusterHumanFormatter; +import org.jboss.jws.diag.modcluster.formatter.ModClusterJsonFormatter; +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; +import org.jboss.jws.diag.summary.discovery.CatalinaDiscovery; +import picocli.CommandLine.Command; +import picocli.CommandLine.Mixin; +import picocli.CommandLine.Option; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +@Command(name = "modcluster", + description = "Display mod_cluster/mod_proxy_cluster configuration from server.xml", + mixinStandardHelpOptions = true) +public class ModClusterCommand implements Runnable { + + @Option(names = "--catalina-home", description = "Path to CATALINA_HOME") + private Path catalinaHome; + + @Option(names = "--catalina-base", description = "Path to CATALINA_BASE (defaults to CATALINA_HOME)") + private Path catalinaBase; + + @Mixin + private OutputFormatMixin outputFormat; + + @Override + public void run() { + Path base = resolveBase(); + if (base == null) { + System.err.println("ERROR: Could not determine CATALINA_BASE. " + + "Use --catalina-home or --catalina-base."); + System.exit(ExitCodes.ERRORS); + return; + } + + Path serverXml = base.resolve("conf/server.xml"); + if (!Files.exists(serverXml)) { + System.err.println("ERROR: server.xml not found at: " + serverXml); + System.exit(ExitCodes.ERRORS); + return; + } + + List configs; + try { + configs = new ModClusterParser().parse(serverXml); + } catch (IOException e) { + System.err.println("ERROR: Failed to parse server.xml: " + e.getMessage()); + System.exit(ExitCodes.ERRORS); + return; + } + + String output; + if (outputFormat.getFormat() == OutputFormat.JSON) { + output = new ModClusterJsonFormatter().format(configs); + } else { + output = new ModClusterHumanFormatter().format(configs); + } + + System.out.println(output); + System.exit(configs.isEmpty() ? ExitCodes.WARNINGS : ExitCodes.OK); + } + + private Path resolveBase() { + if (catalinaBase != null) { + if (!Files.isDirectory(catalinaBase)) { + System.err.println("ERROR: --catalina-base is not a valid directory: " + catalinaBase); + System.exit(ExitCodes.ERRORS); + } + return catalinaBase; + } + if (catalinaHome != null && !Files.isDirectory(catalinaHome)) { + System.err.println("ERROR: --catalina-home is not a valid directory: " + catalinaHome); + System.exit(ExitCodes.ERRORS); + } + CatalinaDiscovery.Result result = CatalinaDiscovery.create(catalinaHome, null).discover(); + return result.getCatalinaBase(); + } +} diff --git a/src/main/java/org/jboss/jws/diag/modcluster/ModClusterParser.java b/src/main/java/org/jboss/jws/diag/modcluster/ModClusterParser.java new file mode 100644 index 0000000..4de39a2 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/modcluster/ModClusterParser.java @@ -0,0 +1,116 @@ +package org.jboss.jws.diag.modcluster; + +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Scans {@code server.xml} for {@code } elements whose + * {@code className} contains {@code "ModCluster"} and extracts their + * configuration into {@link ModClusterConfig} objects. + * + *

Parses independently of the main {@code ServerXmlParser} to avoid any + * coupling with the config command's data model. + */ +public final class ModClusterParser { + + private static final String MOD_CLUSTER_MARKER = "ModCluster"; + + /** Known attributes handled explicitly; everything else goes into extraAttributes. */ + private static final java.util.Set KNOWN_ATTRS = java.util.Set.of( + "className", "connector", "advertise", "advertiseGroupAddress", + "advertisePort", "proxyList", "balancer", "stickySession", "stickySessionCookie" + ); + + /** + * Parses the given {@code server.xml} and returns all mod_cluster listener + * configurations found. Returns an empty list if none are present. + * + * @throws IOException if the file cannot be read or parsed + */ + public List parse(Path serverXml) throws IOException { + Document doc = parseXml(serverXml); + List results = new ArrayList<>(); + + NodeList listeners = doc.getElementsByTagName("Listener"); + for (int i = 0; i < listeners.getLength(); i++) { + Node node = listeners.item(i); + if (!(node instanceof Element)) continue; + Element el = (Element) node; + String className = el.getAttribute("className"); + if (className == null || !className.contains(MOD_CLUSTER_MARKER)) continue; + results.add(extract(el)); + } + return results; + } + + private ModClusterConfig extract(Element el) { + Map extra = new LinkedHashMap<>(); + NamedNodeMap attrs = el.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node attr = attrs.item(i); + String name = attr.getNodeName(); + if (!KNOWN_ATTRS.contains(name)) { + extra.put(name, attr.getNodeValue()); + } + } + + ModClusterConfig.Builder b = ModClusterConfig.builder() + .listenerClassName(el.getAttribute("className")) + .extraAttributes(extra); + + String connector = el.getAttribute("connector"); + if (!connector.isEmpty()) b.connector(connector); + + String advertise = el.getAttribute("advertise"); + if (!advertise.isEmpty()) b.advertise(Boolean.parseBoolean(advertise)); + + String advertiseGroup = el.getAttribute("advertiseGroupAddress"); + if (!advertiseGroup.isEmpty()) b.advertiseGroupAddress(advertiseGroup); + + String advertisePort = el.getAttribute("advertisePort"); + if (!advertisePort.isEmpty()) { + try { b.advertisePort(Integer.parseInt(advertisePort)); } catch (NumberFormatException ignored) {} + } + + String proxyList = el.getAttribute("proxyList"); + if (!proxyList.isEmpty()) b.proxyList(proxyList); + + String balancer = el.getAttribute("balancer"); + if (!balancer.isEmpty()) b.balancer(balancer); + + String stickySession = el.getAttribute("stickySession"); + if (!stickySession.isEmpty()) b.stickySession(Boolean.parseBoolean(stickySession)); + + String stickySessionCookie = el.getAttribute("stickySessionCookie"); + if (!stickySessionCookie.isEmpty()) b.stickySessionCookie(stickySessionCookie); + + return b.build(); + } + + private static Document parseXml(Path path) throws IOException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setErrorHandler(null); + return builder.parse(path.toFile()); + } catch (Exception e) { + throw new IOException("Failed to parse " + path + ": " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/jboss/jws/diag/modcluster/formatter/ModClusterHumanFormatter.java b/src/main/java/org/jboss/jws/diag/modcluster/formatter/ModClusterHumanFormatter.java new file mode 100644 index 0000000..610cc7e --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/modcluster/formatter/ModClusterHumanFormatter.java @@ -0,0 +1,67 @@ +package org.jboss.jws.diag.modcluster.formatter; + +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; + +import java.util.List; +import java.util.Map; + +/** + * Renders mod_cluster configuration as human-readable text. + * + *

Example: + *

+ * mod_cluster Configuration (1 listener)
+ *
+ * Listener: org.jboss.modcluster.container.catalina.standalone.ModClusterListener
+ *   Connector:           ajp
+ *   Advertise:           true (224.0.1.105:23364)
+ *   Proxy list:          httpd1:6666, httpd2:6666
+ *   Balancer:            mycluster
+ *   Sticky sessions:     true (cookie: JSESSIONID)
+ * 
+ */ +public final class ModClusterHumanFormatter { + + public String format(List configs) { + StringBuilder sb = new StringBuilder(); + + if (configs.isEmpty()) { + sb.append("No mod_cluster listener found in server.xml.\n"); + sb.append("Tip: Add to enable mod_cluster.\n"); + return sb.toString(); + } + + sb.append(String.format("mod_cluster Configuration (%d listener%s)%n", + configs.size(), configs.size() == 1 ? "" : "s")); + + for (ModClusterConfig cfg : configs) { + sb.append('\n'); + sb.append(String.format("Listener: %s%n", cfg.getListenerClassName())); + sb.append(String.format(" %-22s %s%n", "Connector:", cfg.getConnector())); + + String advertiseAddr = String.format("%s:%d", + cfg.getAdvertiseGroupAddress(), cfg.getAdvertisePort()); + sb.append(String.format(" %-22s %s (%s)%n", + "Advertise:", cfg.isAdvertise(), advertiseAddr)); + + if (cfg.getProxyList() != null) { + sb.append(String.format(" %-22s %s%n", "Proxy list:", cfg.getProxyList())); + } else { + sb.append(String.format(" %-22s (auto-discover via advertise)%n", "Proxy list:")); + } + + sb.append(String.format(" %-22s %s%n", "Balancer:", cfg.getBalancer())); + sb.append(String.format(" %-22s %s (cookie: %s)%n", + "Sticky sessions:", cfg.isStickySession(), cfg.getStickySessionCookie())); + + if (cfg.getExtraAttributes() != null && !cfg.getExtraAttributes().isEmpty()) { + sb.append(String.format(" %-22s%n", "Additional attributes:")); + for (Map.Entry e : cfg.getExtraAttributes().entrySet()) { + sb.append(String.format(" %-20s %s%n", e.getKey() + ":", e.getValue())); + } + } + } + return sb.toString(); + } +} diff --git a/src/main/java/org/jboss/jws/diag/modcluster/formatter/ModClusterJsonFormatter.java b/src/main/java/org/jboss/jws/diag/modcluster/formatter/ModClusterJsonFormatter.java new file mode 100644 index 0000000..f9de000 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/modcluster/formatter/ModClusterJsonFormatter.java @@ -0,0 +1,43 @@ +package org.jboss.jws.diag.modcluster.formatter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; + +import java.util.List; + +/** + * Serializes mod_cluster configuration as indented JSON. + * + *

Schema: + *

+ * {
+ *   "schemaVersion": "1.0",
+ *   "count": 1,
+ *   "listeners": [ { "listenerClassName": "...", "connector": "ajp", ... } ]
+ * }
+ * 
+ */ +public final class ModClusterJsonFormatter { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + public String format(List configs) { + try { + ObjectNode root = MAPPER.createObjectNode(); + root.put("schemaVersion", "1.0"); + root.put("count", configs.size()); + ArrayNode arr = root.putArray("listeners"); + for (ModClusterConfig cfg : configs) { + arr.add(MAPPER.valueToTree(cfg)); + } + return MAPPER.writeValueAsString(root); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to serialize ModClusterConfig to JSON", e); + } + } +} diff --git a/src/main/java/org/jboss/jws/diag/modcluster/model/ModClusterConfig.java b/src/main/java/org/jboss/jws/diag/modcluster/model/ModClusterConfig.java new file mode 100644 index 0000000..9a3b53c --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/modcluster/model/ModClusterConfig.java @@ -0,0 +1,106 @@ +package org.jboss.jws.diag.modcluster.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.Collections; +import java.util.Map; + +/** + * Configuration extracted from a {@code ModClusterListener} element in {@code server.xml}. + * + *

Default values are Tomcat/mod_cluster compiled-in defaults; only attributes + * explicitly set in server.xml are marked {@code explicit=true} in the raw map. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"listenerClassName", "connector", "advertise", + "advertiseGroupAddress", "advertisePort", "proxyList", + "balancer", "stickySession", "stickySessionCookie", "extraAttributes"}) +public final class ModClusterConfig { + + private final String listenerClassName; + private final String connector; + private final boolean advertise; + private final String advertiseGroupAddress; + private final int advertisePort; + private final String proxyList; + private final String balancer; + private final boolean stickySession; + private final String stickySessionCookie; + private final Map extraAttributes; + + private ModClusterConfig(Builder b) { + this.listenerClassName = b.listenerClassName; + this.connector = b.connector; + this.advertise = b.advertise; + this.advertiseGroupAddress = b.advertiseGroupAddress; + this.advertisePort = b.advertisePort; + this.proxyList = b.proxyList; + this.balancer = b.balancer; + this.stickySession = b.stickySession; + this.stickySessionCookie = b.stickySessionCookie; + this.extraAttributes = b.extraAttributes != null + ? Collections.unmodifiableMap(b.extraAttributes) : Collections.emptyMap(); + } + + @JsonProperty("listenerClassName") + public String getListenerClassName() { return listenerClassName; } + + @JsonProperty("connector") + public String getConnector() { return connector; } + + @JsonProperty("advertise") + public boolean isAdvertise() { return advertise; } + + @JsonProperty("advertiseGroupAddress") + public String getAdvertiseGroupAddress() { return advertiseGroupAddress; } + + @JsonProperty("advertisePort") + public int getAdvertisePort() { return advertisePort; } + + @JsonProperty("proxyList") + public String getProxyList() { return proxyList; } + + @JsonProperty("balancer") + public String getBalancer() { return balancer; } + + @JsonProperty("stickySession") + public boolean isStickySession() { return stickySession; } + + @JsonProperty("stickySessionCookie") + public String getStickySessionCookie() { return stickySessionCookie; } + + @JsonProperty("extraAttributes") + public Map getExtraAttributes() { + return extraAttributes.isEmpty() ? null : extraAttributes; + } + + public static Builder builder() { return new Builder(); } + + public static final class Builder { + private String listenerClassName; + private String connector = "ajp"; + private boolean advertise = true; + private String advertiseGroupAddress = "224.0.1.105"; + private int advertisePort = 23364; + private String proxyList; + private String balancer = "mycluster"; + private boolean stickySession = true; + private String stickySessionCookie = "JSESSIONID"; + private Map extraAttributes; + + public Builder listenerClassName(String v) { this.listenerClassName = v; return this; } + public Builder connector(String v) { this.connector = v; return this; } + public Builder advertise(boolean v) { this.advertise = v; return this; } + public Builder advertiseGroupAddress(String v) { this.advertiseGroupAddress = v; return this; } + public Builder advertisePort(int v) { this.advertisePort = v; return this; } + public Builder proxyList(String v) { this.proxyList = v; return this; } + public Builder balancer(String v) { this.balancer = v; return this; } + public Builder stickySession(boolean v) { this.stickySession = v; return this; } + public Builder stickySessionCookie(String v) { this.stickySessionCookie = v; return this; } + public Builder extraAttributes(Map v) { this.extraAttributes = v; return this; } + + public ModClusterConfig build() { return new ModClusterConfig(this); } + } +} diff --git a/src/test/java/org/jboss/jws/diag/modcluster/ModClusterHumanFormatterTest.java b/src/test/java/org/jboss/jws/diag/modcluster/ModClusterHumanFormatterTest.java new file mode 100644 index 0000000..79d8d34 --- /dev/null +++ b/src/test/java/org/jboss/jws/diag/modcluster/ModClusterHumanFormatterTest.java @@ -0,0 +1,65 @@ +package org.jboss.jws.diag.modcluster; + +import org.jboss.jws.diag.modcluster.formatter.ModClusterHumanFormatter; +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ModClusterHumanFormatterTest { + + private final ModClusterHumanFormatter formatter = new ModClusterHumanFormatter(); + + @Test + void emptyList_showsNotFoundMessage() { + String out = formatter.format(Collections.emptyList()); + assertThat(out).contains("No mod_cluster listener found"); + assertThat(out).contains("Tip:"); + } + + @Test + void singleConfig_showsAllFields() { + ModClusterConfig cfg = ModClusterConfig.builder() + .listenerClassName("org.jboss.modcluster.container.catalina.standalone.ModClusterListener") + .connector("ajp") + .advertise(true) + .advertiseGroupAddress("224.0.1.105") + .advertisePort(23364) + .proxyList("httpd1:6666") + .balancer("mycluster") + .stickySession(true) + .stickySessionCookie("JSESSIONID") + .build(); + + String out = formatter.format(List.of(cfg)); + + assertThat(out).contains("ModClusterListener"); + assertThat(out).contains("ajp"); + assertThat(out).contains("224.0.1.105:23364"); + assertThat(out).contains("httpd1:6666"); + assertThat(out).contains("mycluster"); + assertThat(out).contains("JSESSIONID"); + } + + @Test + void noProxyList_showsAutoDiscoverMessage() { + ModClusterConfig cfg = ModClusterConfig.builder() + .listenerClassName("org.jboss.modcluster.ModClusterListener") + .build(); + + String out = formatter.format(List.of(cfg)); + assertThat(out).contains("auto-discover"); + } + + @Test + void multipleListeners_countShown() { + ModClusterConfig cfg = ModClusterConfig.builder() + .listenerClassName("org.jboss.modcluster.ModClusterListener").build(); + + String out = formatter.format(List.of(cfg, cfg)); + assertThat(out).contains("2 listener"); + } +} diff --git a/src/test/java/org/jboss/jws/diag/modcluster/ModClusterJsonFormatterTest.java b/src/test/java/org/jboss/jws/diag/modcluster/ModClusterJsonFormatterTest.java new file mode 100644 index 0000000..53f80b6 --- /dev/null +++ b/src/test/java/org/jboss/jws/diag/modcluster/ModClusterJsonFormatterTest.java @@ -0,0 +1,70 @@ +package org.jboss.jws.diag.modcluster; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jboss.jws.diag.modcluster.formatter.ModClusterJsonFormatter; +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ModClusterJsonFormatterTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private final ModClusterJsonFormatter formatter = new ModClusterJsonFormatter(); + + private JsonNode parse(List configs) throws Exception { + return MAPPER.readTree(formatter.format(configs)); + } + + @Test + void schemaVersionPresentAtRoot() throws Exception { + JsonNode root = parse(Collections.emptyList()); + assertThat(root.get("schemaVersion").asText()).isEqualTo("1.0"); + } + + @Test + void emptyList_countZeroEmptyArray() throws Exception { + JsonNode root = parse(Collections.emptyList()); + assertThat(root.get("count").asInt()).isEqualTo(0); + assertThat(root.get("listeners").size()).isEqualTo(0); + } + + @Test + void singleConfig_allKnownFieldsPresent() throws Exception { + ModClusterConfig cfg = ModClusterConfig.builder() + .listenerClassName("org.jboss.modcluster.container.catalina.standalone.ModClusterListener") + .connector("ajp") + .proxyList("httpd1:6666") + .balancer("mycluster") + .build(); + + JsonNode entry = parse(List.of(cfg)).get("listeners").get(0); + assertThat(entry.get("listenerClassName").asText()).contains("ModClusterListener"); + assertThat(entry.get("connector").asText()).isEqualTo("ajp"); + assertThat(entry.get("proxyList").asText()).isEqualTo("httpd1:6666"); + assertThat(entry.get("balancer").asText()).isEqualTo("mycluster"); + } + + @Test + void countMatchesListSize() throws Exception { + ModClusterConfig cfg = ModClusterConfig.builder() + .listenerClassName("org.jboss.modcluster.ModClusterListener").build(); + + JsonNode root = parse(List.of(cfg, cfg)); + assertThat(root.get("count").asInt()).isEqualTo(2); + assertThat(root.get("listeners").size()).isEqualTo(2); + } + + @Test + void nullProxyList_fieldAbsentFromJson() throws Exception { + ModClusterConfig cfg = ModClusterConfig.builder() + .listenerClassName("org.jboss.modcluster.ModClusterListener").build(); + + JsonNode entry = parse(List.of(cfg)).get("listeners").get(0); + assertThat(entry.has("proxyList")).isFalse(); + } +} diff --git a/src/test/java/org/jboss/jws/diag/modcluster/ModClusterParserTest.java b/src/test/java/org/jboss/jws/diag/modcluster/ModClusterParserTest.java new file mode 100644 index 0000000..96b000b --- /dev/null +++ b/src/test/java/org/jboss/jws/diag/modcluster/ModClusterParserTest.java @@ -0,0 +1,69 @@ +package org.jboss.jws.diag.modcluster; + +import org.jboss.jws.diag.modcluster.model.ModClusterConfig; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ModClusterParserTest { + + private final ModClusterParser parser = new ModClusterParser(); + + private Path fixture(String name) throws URISyntaxException { + return Paths.get(getClass().getClassLoader() + .getResource("fixtures/modcluster/" + name).toURI()); + } + + @Test + void noModClusterListener_returnsEmptyList() throws Exception { + List result = parser.parse(fixture("server-no-modcluster.xml")); + assertThat(result).isEmpty(); + } + + @Test + void fullConfig_allAttributesExtracted() throws Exception { + List result = parser.parse(fixture("server-modcluster-full.xml")); + + assertThat(result).hasSize(1); + ModClusterConfig cfg = result.get(0); + assertThat(cfg.getListenerClassName()) + .isEqualTo("org.jboss.modcluster.container.catalina.standalone.ModClusterListener"); + assertThat(cfg.getConnector()).isEqualTo("ajp"); + assertThat(cfg.isAdvertise()).isTrue(); + assertThat(cfg.getAdvertiseGroupAddress()).isEqualTo("224.0.1.105"); + assertThat(cfg.getAdvertisePort()).isEqualTo(23364); + assertThat(cfg.getProxyList()).isEqualTo("httpd1.example.com:6666,httpd2.example.com:6666"); + assertThat(cfg.getBalancer()).isEqualTo("prodcluster"); + assertThat(cfg.isStickySession()).isTrue(); + assertThat(cfg.getStickySessionCookie()).isEqualTo("JSESSIONID"); + } + + @Test + void defaultsConfig_knownDefaultsApplied() throws Exception { + List result = parser.parse(fixture("server-modcluster-defaults.xml")); + + assertThat(result).hasSize(1); + ModClusterConfig cfg = result.get(0); + assertThat(cfg.getConnector()).isEqualTo("ajp"); + assertThat(cfg.isAdvertise()).isTrue(); + assertThat(cfg.getAdvertiseGroupAddress()).isEqualTo("224.0.1.105"); + assertThat(cfg.getAdvertisePort()).isEqualTo(23364); + assertThat(cfg.getProxyList()).isNull(); + assertThat(cfg.getBalancer()).isEqualTo("mycluster"); + assertThat(cfg.isStickySession()).isTrue(); + assertThat(cfg.getStickySessionCookie()).isEqualTo("JSESSIONID"); + } + + @Test + void nonExistentFile_throwsIOException() { + assertThatThrownBy(() -> parser.parse(Path.of("/nonexistent/server.xml"))) + .isInstanceOf(IOException.class); + } +} diff --git a/src/test/resources/fixtures/modcluster/server-modcluster-defaults.xml b/src/test/resources/fixtures/modcluster/server-modcluster-defaults.xml new file mode 100644 index 0000000..17d5264 --- /dev/null +++ b/src/test/resources/fixtures/modcluster/server-modcluster-defaults.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/test/resources/fixtures/modcluster/server-modcluster-full.xml b/src/test/resources/fixtures/modcluster/server-modcluster-full.xml new file mode 100644 index 0000000..a45d05e --- /dev/null +++ b/src/test/resources/fixtures/modcluster/server-modcluster-full.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/src/test/resources/fixtures/modcluster/server-no-modcluster.xml b/src/test/resources/fixtures/modcluster/server-no-modcluster.xml new file mode 100644 index 0000000..d30be49 --- /dev/null +++ b/src/test/resources/fixtures/modcluster/server-no-modcluster.xml @@ -0,0 +1,10 @@ + + + + + + + + + +