Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/org/jboss/jws/diag/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +20,7 @@
ValidateCommand.class,
BundleCommand.class,
LogsCommand.class,
ModClusterCommand.class,
DiffCommand.class
}
)
Expand Down
85 changes: 85 additions & 0 deletions src/main/java/org/jboss/jws/diag/modcluster/ModClusterCommand.java
Original file line number Diff line number Diff line change
@@ -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<ModClusterConfig> 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();
}
}
116 changes: 116 additions & 0 deletions src/main/java/org/jboss/jws/diag/modcluster/ModClusterParser.java
Original file line number Diff line number Diff line change
@@ -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 <Listener>} elements whose
* {@code className} contains {@code "ModCluster"} and extracts their
* configuration into {@link ModClusterConfig} objects.
*
* <p>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<String> 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<ModClusterConfig> parse(Path serverXml) throws IOException {
Document doc = parseXml(serverXml);
List<ModClusterConfig> 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<String, String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Example:
* <pre>
* 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)
* </pre>
*/
public final class ModClusterHumanFormatter {

public String format(List<ModClusterConfig> configs) {
StringBuilder sb = new StringBuilder();

if (configs.isEmpty()) {
sb.append("No mod_cluster listener found in server.xml.\n");
sb.append("Tip: Add <Listener className=\"org.jboss.modcluster.container.catalina."
+ "standalone.ModClusterListener\" .../> 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<String, String> e : cfg.getExtraAttributes().entrySet()) {
sb.append(String.format(" %-20s %s%n", e.getKey() + ":", e.getValue()));
}
}
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Schema:
* <pre>
* {
* "schemaVersion": "1.0",
* "count": 1,
* "listeners": [ { "listenerClassName": "...", "connector": "ajp", ... } ]
* }
* </pre>
*/
public final class ModClusterJsonFormatter {

private static final ObjectMapper MAPPER = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);

public String format(List<ModClusterConfig> 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);
}
}
}
Loading
Loading