diff --git a/src/main/java/org/jboss/jws/diag/summary/model/ContainerInfo.java b/src/main/java/org/jboss/jws/diag/summary/model/ContainerInfo.java new file mode 100644 index 0000000..e9c15c5 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/summary/model/ContainerInfo.java @@ -0,0 +1,54 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Container runtime detected for the running Tomcat process. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class ContainerInfo { + + private final ContainerType type; + private final String detectionMethod; + + private ContainerInfo(Builder builder) { + this.type = builder.type; + this.detectionMethod = builder.detectionMethod; + } + + public ContainerType getType() { + return type; + } + + public String getDetectionMethod() { + return detectionMethod; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private ContainerType type; + private String detectionMethod; + + public Builder type(ContainerType type) { + this.type = type; + return this; + } + + public Builder detectionMethod(String detectionMethod) { + this.detectionMethod = detectionMethod; + return this; + } + + public ContainerInfo build() { + return new ContainerInfo(this); + } + } + + @Override + public String toString() { + return "ContainerInfo{type=" + type + ", detectionMethod='" + detectionMethod + "'}"; + } +} diff --git a/src/main/java/org/jboss/jws/diag/summary/model/ContainerType.java b/src/main/java/org/jboss/jws/diag/summary/model/ContainerType.java new file mode 100644 index 0000000..6574e45 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/summary/model/ContainerType.java @@ -0,0 +1,25 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Container runtime in which the Tomcat process is running. + */ +public enum ContainerType { + + DOCKER("docker"), + PODMAN("podman"), + KUBERNETES("kubernetes"), + BARE_METAL("bare_metal"); + + private final String value; + + ContainerType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/org/jboss/jws/diag/summary/model/JvmInfo.java b/src/main/java/org/jboss/jws/diag/summary/model/JvmInfo.java new file mode 100644 index 0000000..7237719 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/summary/model/JvmInfo.java @@ -0,0 +1,93 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +/** + * JVM information collected from system properties and the running process. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JvmInfo { + + private final String version; + private final String vendor; + private final Path javaHome; + /** + * JVM args as seen in {@code /proc//cmdline}. Callers must redact sensitive + * {@code -D} flags (e.g. {@code -Djavax.net.ssl.keyStorePassword}) before passing + * this list to the builder. + */ + private final List jvmArgs; + + private JvmInfo(Builder builder) { + this.version = builder.version; + this.vendor = builder.vendor; + this.javaHome = builder.javaHome; + this.jvmArgs = builder.jvmArgs != null + ? Collections.unmodifiableList(builder.jvmArgs) + : null; + } + + public String getVersion() { + return version; + } + + public String getVendor() { + return vendor; + } + + @JsonSerialize(using = ToStringSerializer.class) + public Path getJavaHome() { + return javaHome; + } + + public List getJvmArgs() { + return jvmArgs; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String version; + private String vendor; + private Path javaHome; + private List jvmArgs; + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder vendor(String vendor) { + this.vendor = vendor; + return this; + } + + public Builder javaHome(Path javaHome) { + this.javaHome = javaHome; + return this; + } + + public Builder jvmArgs(List jvmArgs) { + this.jvmArgs = jvmArgs; + return this; + } + + public JvmInfo build() { + return new JvmInfo(this); + } + } + + @Override + public String toString() { + return "JvmInfo{version='" + version + "', vendor='" + vendor + + "', javaHome=" + javaHome + ", jvmArgs=" + jvmArgs + '}'; + } +} diff --git a/src/main/java/org/jboss/jws/diag/summary/model/JwsInstallation.java b/src/main/java/org/jboss/jws/diag/summary/model/JwsInstallation.java new file mode 100644 index 0000000..52e78c7 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/summary/model/JwsInstallation.java @@ -0,0 +1,175 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.nio.file.Path; + +/** + * Root model representing a discovered JBoss Web Server / Apache Tomcat installation. + * All fields except {@code schemaVersion} are optional; discovery may populate a subset + * depending on what is detectable in the environment. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class JwsInstallation { + + private static final String SCHEMA_VERSION = "1.0"; + + private final Path catalinaHome; + private final Path catalinaBase; + private final String tomcatVersion; + private final String jwsVersion; + private final JvmInfo jvmInfo; + private final OsInfo osInfo; + private final ContainerInfo containerInfo; + private final NativeInfo nativeInfo; + private final Integer pid; + private final String uptime; + + private JwsInstallation(Builder builder) { + this.catalinaHome = builder.catalinaHome; + this.catalinaBase = builder.catalinaBase; + this.tomcatVersion = builder.tomcatVersion; + this.jwsVersion = builder.jwsVersion; + this.jvmInfo = builder.jvmInfo; + this.osInfo = builder.osInfo; + this.containerInfo = builder.containerInfo; + this.nativeInfo = builder.nativeInfo; + this.pid = builder.pid; + this.uptime = builder.uptime; + } + + public String getSchemaVersion() { + return SCHEMA_VERSION; + } + + @JsonSerialize(using = ToStringSerializer.class) + public Path getCatalinaHome() { + return catalinaHome; + } + + @JsonSerialize(using = ToStringSerializer.class) + public Path getCatalinaBase() { + return catalinaBase; + } + + public String getTomcatVersion() { + return tomcatVersion; + } + + public String getJwsVersion() { + return jwsVersion; + } + + public JvmInfo getJvmInfo() { + return jvmInfo; + } + + public OsInfo getOsInfo() { + return osInfo; + } + + public ContainerInfo getContainerInfo() { + return containerInfo; + } + + public NativeInfo getNativeInfo() { + return nativeInfo; + } + + public Integer getPid() { + return pid; + } + + public String getUptime() { + return uptime; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Path catalinaHome; + private Path catalinaBase; + private String tomcatVersion; + private String jwsVersion; + private JvmInfo jvmInfo; + private OsInfo osInfo; + private ContainerInfo containerInfo; + private NativeInfo nativeInfo; + private Integer pid; + private String uptime; + + public Builder catalinaHome(Path catalinaHome) { + this.catalinaHome = catalinaHome; + return this; + } + + public Builder catalinaBase(Path catalinaBase) { + this.catalinaBase = catalinaBase; + return this; + } + + public Builder tomcatVersion(String tomcatVersion) { + this.tomcatVersion = tomcatVersion; + return this; + } + + public Builder jwsVersion(String jwsVersion) { + this.jwsVersion = jwsVersion; + return this; + } + + public Builder jvmInfo(JvmInfo jvmInfo) { + this.jvmInfo = jvmInfo; + return this; + } + + public Builder osInfo(OsInfo osInfo) { + this.osInfo = osInfo; + return this; + } + + public Builder containerInfo(ContainerInfo containerInfo) { + this.containerInfo = containerInfo; + return this; + } + + public Builder nativeInfo(NativeInfo nativeInfo) { + this.nativeInfo = nativeInfo; + return this; + } + + public Builder pid(Integer pid) { + this.pid = pid; + return this; + } + + public Builder uptime(String uptime) { + this.uptime = uptime; + return this; + } + + public JwsInstallation build() { + return new JwsInstallation(this); + } + } + + @Override + public String toString() { + return "JwsInstallation{" + + "catalinaHome=" + catalinaHome + + ", catalinaBase=" + catalinaBase + + ", tomcatVersion='" + tomcatVersion + '\'' + + ", jwsVersion='" + jwsVersion + '\'' + + ", pid=" + pid + + ", uptime='" + uptime + '\'' + + ", jvmInfo=" + jvmInfo + + ", osInfo=" + osInfo + + ", containerInfo=" + containerInfo + + ", nativeInfo=" + nativeInfo + + '}'; + } +} diff --git a/src/main/java/org/jboss/jws/diag/summary/model/NativeInfo.java b/src/main/java/org/jboss/jws/diag/summary/model/NativeInfo.java new file mode 100644 index 0000000..0fec2f7 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/summary/model/NativeInfo.java @@ -0,0 +1,67 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Information about native libraries (APR, OpenSSL) loaded by Tomcat. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class NativeInfo { + + private final String aprVersion; + private final String opensslVersion; + private final Boolean loaded; + + private NativeInfo(Builder builder) { + this.aprVersion = builder.aprVersion; + this.opensslVersion = builder.opensslVersion; + this.loaded = builder.loaded; + } + + public String getAprVersion() { + return aprVersion; + } + + public String getOpensslVersion() { + return opensslVersion; + } + + public Boolean isLoaded() { + return loaded; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String aprVersion; + private String opensslVersion; + private Boolean loaded; + + public Builder aprVersion(String aprVersion) { + this.aprVersion = aprVersion; + return this; + } + + public Builder opensslVersion(String opensslVersion) { + this.opensslVersion = opensslVersion; + return this; + } + + public Builder loaded(Boolean loaded) { + this.loaded = loaded; + return this; + } + + public NativeInfo build() { + return new NativeInfo(this); + } + } + + @Override + public String toString() { + return "NativeInfo{aprVersion='" + aprVersion + "', opensslVersion='" + opensslVersion + + "', loaded=" + loaded + '}'; + } +} diff --git a/src/main/java/org/jboss/jws/diag/summary/model/OsInfo.java b/src/main/java/org/jboss/jws/diag/summary/model/OsInfo.java new file mode 100644 index 0000000..aa1bc54 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/summary/model/OsInfo.java @@ -0,0 +1,66 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Operating system information collected from {@code /etc/os-release} or system properties. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class OsInfo { + + private final String name; + private final String version; + private final String arch; + + private OsInfo(Builder builder) { + this.name = builder.name; + this.version = builder.version; + this.arch = builder.arch; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public String getArch() { + return arch; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String name; + private String version; + private String arch; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder arch(String arch) { + this.arch = arch; + return this; + } + + public OsInfo build() { + return new OsInfo(this); + } + } + + @Override + public String toString() { + return "OsInfo{name='" + name + "', version='" + version + "', arch='" + arch + "'}"; + } +} diff --git a/src/test/java/org/jboss/jws/diag/summary/model/JwsInstallationModelTest.java b/src/test/java/org/jboss/jws/diag/summary/model/JwsInstallationModelTest.java new file mode 100644 index 0000000..a4550c6 --- /dev/null +++ b/src/test/java/org/jboss/jws/diag/summary/model/JwsInstallationModelTest.java @@ -0,0 +1,163 @@ +package org.jboss.jws.diag.summary.model; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class JwsInstallationModelTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + // --- JwsInstallation --- + + @Test + void schemaVersionIsAlways10() throws Exception { + JwsInstallation inst = JwsInstallation.builder().build(); + JsonNode json = mapper.valueToTree(inst); + assertThat(json.get("schemaVersion").asText()).isEqualTo("1.0"); + } + + @Test + void nullFieldsAreExcludedFromJson() throws Exception { + JwsInstallation inst = JwsInstallation.builder() + .tomcatVersion("10.1.49") + .build(); + JsonNode json = mapper.valueToTree(inst); + assertThat(json.has("catalinaHome")).isFalse(); + assertThat(json.has("catalinaBase")).isFalse(); + assertThat(json.has("jwsVersion")).isFalse(); + assertThat(json.has("pid")).isFalse(); + assertThat(json.has("uptime")).isFalse(); + assertThat(json.get("tomcatVersion").asText()).isEqualTo("10.1.49"); + } + + @Test + void pathFieldsSerializeAsPlainStrings() throws Exception { + Path home = Paths.get("/opt/tomcat"); + Path base = Paths.get("/opt/tomcat/conf"); + JwsInstallation inst = JwsInstallation.builder() + .catalinaHome(home) + .catalinaBase(base) + .build(); + JsonNode json = mapper.valueToTree(inst); + assertThat(json.get("catalinaHome").asText()).isEqualTo("/opt/tomcat"); + assertThat(json.get("catalinaBase").asText()).isEqualTo("/opt/tomcat/conf"); + } + + @Test + void builderPopulatesAllFields() { + JvmInfo jvm = JvmInfo.builder().version("17.0.10").build(); + OsInfo os = OsInfo.builder().name("RHEL").version("9.3").arch("x86_64").build(); + ContainerInfo container = ContainerInfo.builder() + .type(ContainerType.PODMAN) + .detectionMethod("/run/.containerenv") + .build(); + NativeInfo native_ = NativeInfo.builder() + .aprVersion("1.7.2") + .opensslVersion("3.0.9") + .loaded(true) + .build(); + + JwsInstallation inst = JwsInstallation.builder() + .catalinaHome(Paths.get("/opt/tomcat")) + .catalinaBase(Paths.get("/etc/tomcat")) + .tomcatVersion("10.1.49") + .jwsVersion("6.1.0") + .jvmInfo(jvm) + .osInfo(os) + .containerInfo(container) + .nativeInfo(native_) + .pid(12345) + .uptime("2d 4h") + .build(); + + assertThat(inst.getTomcatVersion()).isEqualTo("10.1.49"); + assertThat(inst.getJwsVersion()).isEqualTo("6.1.0"); + assertThat(inst.getPid()).isEqualTo(12345); + assertThat(inst.getUptime()).isEqualTo("2d 4h"); + assertThat(inst.getJvmInfo()).isSameAs(jvm); + assertThat(inst.getOsInfo()).isSameAs(os); + assertThat(inst.getContainerInfo()).isSameAs(container); + assertThat(inst.getNativeInfo()).isSameAs(native_); + } + + // --- JvmInfo --- + + @Test + void jvmInfoJavaHomeSerializesAsString() throws Exception { + Path javaHome = Paths.get("/usr/lib/jvm/java-17"); + JvmInfo jvm = JvmInfo.builder() + .version("17.0.10") + .vendor("Red Hat") + .javaHome(javaHome) + .build(); + JsonNode json = mapper.valueToTree(jvm); + assertThat(json.get("javaHome").asText()).isEqualTo("/usr/lib/jvm/java-17"); + } + + @Test + void jvmInfoNullFieldsExcluded() throws Exception { + JvmInfo jvm = JvmInfo.builder().version("17.0.10").build(); + JsonNode json = mapper.valueToTree(jvm); + assertThat(json.has("vendor")).isFalse(); + assertThat(json.has("javaHome")).isFalse(); + assertThat(json.has("jvmArgs")).isFalse(); + } + + @Test + void jvmArgsListIsImmutable() { + List args = Arrays.asList("-Xmx512m", "-Xms256m"); + JvmInfo jvm = JvmInfo.builder().jvmArgs(args).build(); + assertThat(jvm.getJvmArgs()).containsExactly("-Xmx512m", "-Xms256m"); + } + + // --- NativeInfo --- + + @Test + void nativeInfoLoadedNullIsExcludedFromJson() throws Exception { + NativeInfo native_ = NativeInfo.builder() + .aprVersion("1.7.2") + .build(); + assertThat(native_.isLoaded()).isNull(); + JsonNode json = mapper.valueToTree(native_); + assertThat(json.has("loaded")).isFalse(); + } + + @Test + void nativeInfoLoadedFalseIsIncludedInJson() throws Exception { + NativeInfo native_ = NativeInfo.builder().loaded(false).build(); + JsonNode json = mapper.valueToTree(native_); + assertThat(json.get("loaded").asBoolean()).isFalse(); + } + + // --- OsInfo --- + + @Test + void osInfoBuilderAndSerialization() throws Exception { + OsInfo os = OsInfo.builder().name("RHEL").version("9.3").arch("x86_64").build(); + JsonNode json = mapper.valueToTree(os); + assertThat(json.get("name").asText()).isEqualTo("RHEL"); + assertThat(json.get("version").asText()).isEqualTo("9.3"); + assertThat(json.get("arch").asText()).isEqualTo("x86_64"); + } + + // --- ContainerInfo --- + + @Test + void containerInfoEnumSerializesAsLowercaseString() throws Exception { + ContainerInfo info = ContainerInfo.builder() + .type(ContainerType.DOCKER) + .detectionMethod("/.dockerenv") + .build(); + JsonNode json = mapper.valueToTree(info); + assertThat(json.get("type").asText()).isEqualTo("docker"); + assertThat(json.get("detectionMethod").asText()).isEqualTo("/.dockerenv"); + } +}