From af14f33eaf707c81613d412ba759a835f7d1ae3d Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Mon, 1 Dec 2025 16:25:49 +0100 Subject: [PATCH 01/10] Round 1: Add generation --- hivemq-edge/build.gradle.kts | 21 ++ hivemq-edge/docs/xsd-generation-plan.md | 258 ++++++++++++++++++ .../fieldmapping/InstructionEntity.java | 5 + .../entity/combining/DataCombinerEntity.java | 6 +- .../DataCombiningDestinationEntity.java | 5 + .../entity/combining/DataCombiningEntity.java | 6 +- .../combining/DataCombiningSourcesEntity.java | 6 +- .../DataIdentifierReferenceEntity.java | 5 + .../combining/EntityReferenceEntity.java | 5 + .../hivemq/configuration/GenSchemaMain.java | 164 ++++++++++- 10 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 hivemq-edge/docs/xsd-generation-plan.md diff --git a/hivemq-edge/build.gradle.kts b/hivemq-edge/build.gradle.kts index 21503fe911..dc86849750 100644 --- a/hivemq-edge/build.gradle.kts +++ b/hivemq-edge/build.gradle.kts @@ -605,3 +605,24 @@ artifacts { add(releaseJar.name, tasks.shadowJar) add(thirdPartyLicenses.name, tasks.updateThirdPartyLicenses.flatMap { it.outputDirectory }) } + +/* ******************** XSD Generation ******************** */ + +val generateXsd by tasks.registering(JavaExec::class) { + group = "build" + description = "Generates XSD schema from JAXB-annotated configuration entity classes" + + dependsOn(tasks.testClasses) + + mainClass.set("com.hivemq.configuration.GenSchemaMain") + classpath = sourceSets.test.get().runtimeClasspath + + val outputFile = layout.buildDirectory.file("generated-xsd/config-generated.xsd") + args(outputFile.get().asFile.absolutePath) + + outputs.file(outputFile) + + doFirst { + outputFile.get().asFile.parentFile.mkdirs() + } +} diff --git a/hivemq-edge/docs/xsd-generation-plan.md b/hivemq-edge/docs/xsd-generation-plan.md new file mode 100644 index 0000000000..9272a1da53 --- /dev/null +++ b/hivemq-edge/docs/xsd-generation-plan.md @@ -0,0 +1,258 @@ +# Plan: Fix XSD Auto-Generation for Backwards Compatibility + +## Problem Summary + +The current JAXB-generated XSD is **not backwards compatible** with existing configuration files due to three main issues: + +1. **Element ordering**: JAXB generates `xs:sequence` (strict order) but configs use elements in arbitrary order +2. **Listener types**: Generated XSD doesn't properly express the listener type hierarchy (`tcp-listener`, `tls-tcp-listener`, `websocket-listener`, etc.) +3. **Protocol adapters**: Generated XSD expects `` elements, but legacy configs use adapter-specific elements like `` + +## Solution Approach + +Since JAXB has inherent limitations that cannot be overcome through annotations alone, the solution is to **post-process the generated XSD** to make it backwards compatible. This preserves the benefit of auto-generation while ensuring compatibility. + +--- + +## Implementation Plan + +### Phase 1: Enhance Post-Processing in GenSchemaMain.java + +#### Task 1.1: Replace `xs:sequence` with `xs:all` for Root Entity + +**Location**: `GenSchemaMain.java` - `addCustomSimpleTypes()` method (rename to `postProcessSchema()`) + +**What to do**: +- Find `` +- Replace `` with `` +- Replace closing `` with `` + +**Why**: `xs:all` allows child elements in any order, which is how the manual XSD works and how users write configs. + +**Limitation**: `xs:all` doesn't support `maxOccurs="unbounded"` on child elements, so wrapper elements like `mqtt-listeners` that contain lists must remain as `xs:sequence` internally. + +#### Task 1.2: Fix Listener Type Polymorphism + +**Location**: `GenSchemaMain.java` post-processing + +**What to do**: +Replace the generated mqtt-listeners element: +```xml + + + + + + + +``` + +With a proper choice of listener types: +```xml + + + + + + + + + + +``` + +**Why**: The Java code uses `@XmlElementRef` on `List` which generates a reference to the abstract base, not the concrete subclasses. + +#### Task 1.3: Fix Protocol Adapters to Use xs:any + +**Location**: `GenSchemaMain.java` post-processing + +**What to do**: +Replace the generated protocol-adapters element: +```xml + + + + + + + +``` + +With: +```xml + + + + + + + + + + +``` + +**Why**: Protocol adapters have dynamic schemas (each adapter type like `simulation`, `modbus`, `opcua` has its own structure). The `xs:any` allows legacy adapter-specific elements while `protocol-adapter` supports the new unified format. + +#### Task 1.4: Fix Modules to Use xs:any + +**Location**: `GenSchemaMain.java` post-processing + +**What to do**: +Ensure modules element uses `xs:any`: +```xml + + + + + + + +``` + +**Why**: Modules are extensible and have arbitrary structures. + +--- + +### Phase 2: Implementation Details + +#### Task 2.1: Refactor GenSchemaMain.java + +Create a new method structure: + +```java +public class GenSchemaMain { + + public static void generateSchema(File outputFile) throws JAXBException, IOException { + // 1. Generate base schema to temp file + File tempFile = generateBaseSchema(); + + // 2. Post-process for compatibility + String schema = Files.readString(tempFile.toPath()); + schema = replaceSequenceWithAll(schema); + schema = fixListenerTypes(schema); + schema = fixProtocolAdapters(schema); + schema = fixModules(schema); + schema = addCustomSimpleTypes(schema); + + // 3. Write final schema + Files.writeString(outputFile.toPath(), schema); + } + + private static String replaceSequenceWithAll(String schema) { + // Replace xs:sequence with xs:all in hiveMQConfigEntity + // Must be careful to only replace the outer sequence, not nested ones + } + + private static String fixListenerTypes(String schema) { + // Replace address refs with proper listener type choices + } + + private static String fixProtocolAdapters(String schema) { + // Add xs:any for legacy adapter support + } + + private static String fixModules(String schema) { + // Ensure modules uses xs:any + } +} +``` + +#### Task 2.2: Use XML DOM for Reliable Transformations + +Instead of string manipulation, use proper XML DOM parsing: + +```java +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +private static Document loadSchema(File file) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + return factory.newDocumentBuilder().parse(file); +} + +private static void transformSchema(Document doc) { + // Find and modify specific elements using XPath or DOM traversal +} +``` + +**Why**: String replacement is fragile and error-prone. DOM manipulation is more reliable. + +--- + +### Phase 3: Validation + +#### Task 3.1: Create Validation Test + +Add a test that validates all existing configs against the generated XSD: + +```java +@Test +void generatedXsdShouldValidateAllExistingConfigs() throws Exception { + File generatedXsd = new File("build/generated-xsd/config-generated.xsd"); + GenSchemaMain.generateSchema(generatedXsd); + + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + Schema schema = factory.newSchema(generatedXsd); + Validator validator = schema.newValidator(); + + List configFiles = findAllConfigFiles(); + for (File config : configFiles) { + validator.validate(new StreamSource(config)); + } +} +``` + +#### Task 3.2: Add CI Check + +Ensure the Gradle task runs as part of CI and validates: +1. Generated XSD is well-formed +2. All test configs validate against it +3. Generated XSD is consistent (no unexpected changes) + +--- + +## Files to Modify + +| File | Changes | +|------|---------| +| `hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java` | Major refactoring to add post-processing | +| `hivemq-edge/build.gradle.kts` | Add validation task (optional) | + +## Files NOT to Modify + +- Entity classes (`*Entity.java`) - The JAXB annotations are correct for runtime parsing +- Manual XSD (`config.xsd`) - Should eventually be replaced by generated one + +--- + +## Testing Checklist + +After implementation, verify these configs validate: +- [ ] `src/main/resources/config.xml` +- [ ] `src/test/resources/configs/simulation/*.xml` +- [ ] `src/test/resources/configs/testing/*.xml` +- [ ] `src/distribution/conf/examples/**/*.xml` + +--- + +## Risks and Mitigations + +| Risk | Mitigation | +|------|------------| +| DOM manipulation could break valid XSD | Write comprehensive tests first | +| xs:all has limitations (no unbounded) | Keep xs:sequence for wrapper elements | +| Post-processing is complex | Use well-tested XML libraries | +| Schema changes could break existing users | Validate against all known config files | + +--- + +## Future Considerations + +1. **Schema versioning**: Consider adding version attribute to track schema evolution +2. **Deprecation**: Once generated XSD is proven, deprecate manual XSD maintenance +3. **Documentation**: Add XSD documentation annotations from Javadoc diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/adapter/fieldmapping/InstructionEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/adapter/fieldmapping/InstructionEntity.java index 77156d4589..b8578e7be9 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/adapter/fieldmapping/InstructionEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/adapter/fieldmapping/InstructionEntity.java @@ -24,11 +24,16 @@ import org.jetbrains.annotations.Nullable; import jakarta.xml.bind.ValidationEvent; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; import java.util.List; import java.util.Objects; import java.util.Optional; +@XmlRootElement(name = "instruction") +@XmlAccessorType(XmlAccessType.NONE) public class InstructionEntity implements EntityValidatable { @JsonProperty("source") diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombinerEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombinerEntity.java index 500e182524..a8f616522b 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombinerEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombinerEntity.java @@ -20,14 +20,18 @@ import org.jetbrains.annotations.Nullable; import jakarta.xml.bind.ValidationEvent; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementWrapper; +import jakarta.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.UUID; - +@XmlRootElement(name = "data-combiner") +@XmlAccessorType(XmlAccessType.NONE) public class DataCombinerEntity { @JsonProperty(value = "id", required = true) @XmlElement(name = "id", required = true) diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningDestinationEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningDestinationEntity.java index e35497d779..8a29417e78 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningDestinationEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningDestinationEntity.java @@ -16,12 +16,17 @@ package com.hivemq.configuration.entity.combining; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; +@XmlRootElement(name = "destination") +@XmlAccessorType(XmlAccessType.NONE) public class DataCombiningDestinationEntity { @JsonProperty(value = "assetId") diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningEntity.java index 22fc84cfd0..99278cbc70 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningEntity.java @@ -20,15 +20,19 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementWrapper; +import jakarta.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.UUID; - // JAXB can not handle records ... :-( +@XmlRootElement(name = "data-combining") +@XmlAccessorType(XmlAccessType.NONE) public class DataCombiningEntity { @JsonProperty(value = "id", required = true) diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningSourcesEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningSourcesEntity.java index fd587ca7e5..cd22cf44ea 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningSourcesEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataCombiningSourcesEntity.java @@ -19,14 +19,18 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementWrapper; +import jakarta.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; - +@XmlRootElement(name = "sources") +@XmlAccessorType(XmlAccessType.NONE) public class DataCombiningSourcesEntity { @JsonProperty("primaryReference") diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataIdentifierReferenceEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataIdentifierReferenceEntity.java index 2fab6d3cd5..ed2382de8a 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataIdentifierReferenceEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/DataIdentifierReferenceEntity.java @@ -21,10 +21,15 @@ import org.jetbrains.annotations.NotNull; import jakarta.xml.bind.ValidationEvent; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; import java.util.List; import java.util.Objects; +@XmlRootElement(name = "primary-reference") +@XmlAccessorType(XmlAccessType.NONE) public class DataIdentifierReferenceEntity implements EntityValidatable { @JsonProperty("id") diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/EntityReferenceEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/EntityReferenceEntity.java index f31424b41a..7a305a2f9b 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/EntityReferenceEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/combining/EntityReferenceEntity.java @@ -20,9 +20,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; import java.util.Objects; +@XmlRootElement(name = "entity-reference") +@XmlAccessorType(XmlAccessType.NONE) public class EntityReferenceEntity { @JsonProperty(value = "type", required = true) diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index c20f849ae4..b5350fd7e7 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -17,26 +17,180 @@ package com.hivemq.configuration; import com.hivemq.configuration.entity.HiveMQConfigEntity; +import com.hivemq.configuration.entity.adapter.fieldmapping.InstructionEntity; +import com.hivemq.configuration.entity.combining.DataCombinerEntity; +import com.hivemq.configuration.entity.combining.DataCombiningDestinationEntity; +import com.hivemq.configuration.entity.combining.DataCombiningEntity; +import com.hivemq.configuration.entity.combining.DataCombiningSourcesEntity; +import com.hivemq.configuration.entity.combining.DataIdentifierReferenceEntity; +import com.hivemq.configuration.entity.combining.EntityReferenceEntity; import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.SchemaOutputResolver; import javax.xml.transform.Result; import javax.xml.transform.stream.StreamResult; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; +/** + * Utility class to generate XSD schema from JAXB-annotated configuration entity classes. + *

+ * This generates the structural XSD from the Java annotations and adds custom simple types + * for value constraints that JAXB cannot express. + *

+ * Usage: Run the main method with an optional output file path as argument. + * If no argument is provided, outputs to build/generated-xsd/config-generated.xsd + */ public class GenSchemaMain { - public static void main(String[] args) throws Exception{ - JAXBContext context = JAXBContext.newInstance(HiveMQConfigEntity.class); + + /** + * All classes that need to be included in the schema generation. + * This includes the root entity and all referenced entities that use @XmlRootElement. + */ + private static final Class[] SCHEMA_CLASSES = { + HiveMQConfigEntity.class, + // Data combiner entities (for full XSD compliance) + DataCombinerEntity.class, + DataCombiningEntity.class, + DataCombiningSourcesEntity.class, + DataCombiningDestinationEntity.class, + DataIdentifierReferenceEntity.class, + EntityReferenceEntity.class, + InstructionEntity.class + }; + + /** + * Custom simple types to add to the generated schema. + * These provide value constraints that JAXB cannot express through annotations. + */ + private static final String CUSTOM_SIMPLE_TYPES = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + public static void main(String[] args) throws Exception { + String outputPath = args.length > 0 ? args[0] : "build/generated-xsd/config-generated.xsd"; + File outputFile = new File(outputPath); + + // Ensure parent directory exists + File parentDir = outputFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + if (!parentDir.mkdirs()) { + throw new IOException("Failed to create directory: " + parentDir.getAbsolutePath()); + } + } + + generateSchema(outputFile); + System.out.println("XSD schema generated successfully: " + outputFile.getAbsolutePath()); + } + + /** + * Generates the XSD schema to the specified file. + * + * @param outputFile the file to write the schema to + * @throws JAXBException if JAXB context creation fails + * @throws IOException if file writing fails + */ + public static void generateSchema(File outputFile) throws JAXBException, IOException { + // Generate to a temporary file first + File tempFile = File.createTempFile("schema", ".xsd"); + tempFile.deleteOnExit(); + + JAXBContext context = JAXBContext.newInstance(SCHEMA_CLASSES); context.generateSchema(new SchemaOutputResolver() { @Override public Result createOutput(String namespaceUri, String suggestedFileName) throws IOException { - File file = new File("/tmp/schema.xsd"); - StreamResult result = new StreamResult(file); - result.setSystemId(file.toURI().toString()); + StreamResult result = new StreamResult(tempFile); + result.setSystemId(tempFile.toURI().toString()); return result; } }); + + // Post-process to add custom simple types + addCustomSimpleTypes(tempFile, outputFile); + } + + /** + * Adds custom simple types to the generated XSD by inserting them before the closing schema tag. + * + * @param inputFile the generated XSD file + * @param outputFile the final output file with custom types added + * @throws IOException if file operations fail + */ + private static void addCustomSimpleTypes(File inputFile, File outputFile) throws IOException { + String content = Files.readString(inputFile.toPath()); + + // Find the closing tag and insert custom types before it + int closingTagIndex = content.lastIndexOf(""); + if (closingTagIndex == -1) { + throw new IOException("Could not find closing tag in generated XSD"); + } + + String modifiedContent = content.substring(0, closingTagIndex) + + CUSTOM_SIMPLE_TYPES + + content.substring(closingTagIndex); + + Files.writeString(outputFile.toPath(), modifiedContent); } } From be6aa247ba6d2537124e1530134069939fbba9e4 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Mon, 1 Dec 2025 16:37:57 +0100 Subject: [PATCH 02/10] Round 2: closer to original xsd --- hivemq-edge/docs/xsd-generation-plan.md | 258 ---------- .../hivemq/configuration/GenSchemaMain.java | 483 ++++++++++++++---- 2 files changed, 382 insertions(+), 359 deletions(-) delete mode 100644 hivemq-edge/docs/xsd-generation-plan.md diff --git a/hivemq-edge/docs/xsd-generation-plan.md b/hivemq-edge/docs/xsd-generation-plan.md deleted file mode 100644 index 9272a1da53..0000000000 --- a/hivemq-edge/docs/xsd-generation-plan.md +++ /dev/null @@ -1,258 +0,0 @@ -# Plan: Fix XSD Auto-Generation for Backwards Compatibility - -## Problem Summary - -The current JAXB-generated XSD is **not backwards compatible** with existing configuration files due to three main issues: - -1. **Element ordering**: JAXB generates `xs:sequence` (strict order) but configs use elements in arbitrary order -2. **Listener types**: Generated XSD doesn't properly express the listener type hierarchy (`tcp-listener`, `tls-tcp-listener`, `websocket-listener`, etc.) -3. **Protocol adapters**: Generated XSD expects `` elements, but legacy configs use adapter-specific elements like `` - -## Solution Approach - -Since JAXB has inherent limitations that cannot be overcome through annotations alone, the solution is to **post-process the generated XSD** to make it backwards compatible. This preserves the benefit of auto-generation while ensuring compatibility. - ---- - -## Implementation Plan - -### Phase 1: Enhance Post-Processing in GenSchemaMain.java - -#### Task 1.1: Replace `xs:sequence` with `xs:all` for Root Entity - -**Location**: `GenSchemaMain.java` - `addCustomSimpleTypes()` method (rename to `postProcessSchema()`) - -**What to do**: -- Find `` -- Replace `` with `` -- Replace closing `` with `` - -**Why**: `xs:all` allows child elements in any order, which is how the manual XSD works and how users write configs. - -**Limitation**: `xs:all` doesn't support `maxOccurs="unbounded"` on child elements, so wrapper elements like `mqtt-listeners` that contain lists must remain as `xs:sequence` internally. - -#### Task 1.2: Fix Listener Type Polymorphism - -**Location**: `GenSchemaMain.java` post-processing - -**What to do**: -Replace the generated mqtt-listeners element: -```xml - - - - - - - -``` - -With a proper choice of listener types: -```xml - - - - - - - - - - -``` - -**Why**: The Java code uses `@XmlElementRef` on `List` which generates a reference to the abstract base, not the concrete subclasses. - -#### Task 1.3: Fix Protocol Adapters to Use xs:any - -**Location**: `GenSchemaMain.java` post-processing - -**What to do**: -Replace the generated protocol-adapters element: -```xml - - - - - - - -``` - -With: -```xml - - - - - - - - - - -``` - -**Why**: Protocol adapters have dynamic schemas (each adapter type like `simulation`, `modbus`, `opcua` has its own structure). The `xs:any` allows legacy adapter-specific elements while `protocol-adapter` supports the new unified format. - -#### Task 1.4: Fix Modules to Use xs:any - -**Location**: `GenSchemaMain.java` post-processing - -**What to do**: -Ensure modules element uses `xs:any`: -```xml - - - - - - - -``` - -**Why**: Modules are extensible and have arbitrary structures. - ---- - -### Phase 2: Implementation Details - -#### Task 2.1: Refactor GenSchemaMain.java - -Create a new method structure: - -```java -public class GenSchemaMain { - - public static void generateSchema(File outputFile) throws JAXBException, IOException { - // 1. Generate base schema to temp file - File tempFile = generateBaseSchema(); - - // 2. Post-process for compatibility - String schema = Files.readString(tempFile.toPath()); - schema = replaceSequenceWithAll(schema); - schema = fixListenerTypes(schema); - schema = fixProtocolAdapters(schema); - schema = fixModules(schema); - schema = addCustomSimpleTypes(schema); - - // 3. Write final schema - Files.writeString(outputFile.toPath(), schema); - } - - private static String replaceSequenceWithAll(String schema) { - // Replace xs:sequence with xs:all in hiveMQConfigEntity - // Must be careful to only replace the outer sequence, not nested ones - } - - private static String fixListenerTypes(String schema) { - // Replace address refs with proper listener type choices - } - - private static String fixProtocolAdapters(String schema) { - // Add xs:any for legacy adapter support - } - - private static String fixModules(String schema) { - // Ensure modules uses xs:any - } -} -``` - -#### Task 2.2: Use XML DOM for Reliable Transformations - -Instead of string manipulation, use proper XML DOM parsing: - -```java -import javax.xml.parsers.DocumentBuilderFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; - -private static Document loadSchema(File file) throws Exception { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - return factory.newDocumentBuilder().parse(file); -} - -private static void transformSchema(Document doc) { - // Find and modify specific elements using XPath or DOM traversal -} -``` - -**Why**: String replacement is fragile and error-prone. DOM manipulation is more reliable. - ---- - -### Phase 3: Validation - -#### Task 3.1: Create Validation Test - -Add a test that validates all existing configs against the generated XSD: - -```java -@Test -void generatedXsdShouldValidateAllExistingConfigs() throws Exception { - File generatedXsd = new File("build/generated-xsd/config-generated.xsd"); - GenSchemaMain.generateSchema(generatedXsd); - - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - Schema schema = factory.newSchema(generatedXsd); - Validator validator = schema.newValidator(); - - List configFiles = findAllConfigFiles(); - for (File config : configFiles) { - validator.validate(new StreamSource(config)); - } -} -``` - -#### Task 3.2: Add CI Check - -Ensure the Gradle task runs as part of CI and validates: -1. Generated XSD is well-formed -2. All test configs validate against it -3. Generated XSD is consistent (no unexpected changes) - ---- - -## Files to Modify - -| File | Changes | -|------|---------| -| `hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java` | Major refactoring to add post-processing | -| `hivemq-edge/build.gradle.kts` | Add validation task (optional) | - -## Files NOT to Modify - -- Entity classes (`*Entity.java`) - The JAXB annotations are correct for runtime parsing -- Manual XSD (`config.xsd`) - Should eventually be replaced by generated one - ---- - -## Testing Checklist - -After implementation, verify these configs validate: -- [ ] `src/main/resources/config.xml` -- [ ] `src/test/resources/configs/simulation/*.xml` -- [ ] `src/test/resources/configs/testing/*.xml` -- [ ] `src/distribution/conf/examples/**/*.xml` - ---- - -## Risks and Mitigations - -| Risk | Mitigation | -|------|------------| -| DOM manipulation could break valid XSD | Write comprehensive tests first | -| xs:all has limitations (no unbounded) | Keep xs:sequence for wrapper elements | -| Post-processing is complex | Use well-tested XML libraries | -| Schema changes could break existing users | Validate against all known config files | - ---- - -## Future Considerations - -1. **Schema versioning**: Consider adding version attribute to track schema evolution -2. **Deprecation**: Once generated XSD is proven, deprecate manual XSD maintenance -3. **Documentation**: Add XSD documentation annotations from Javadoc diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index b5350fd7e7..1364145092 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -24,115 +24,79 @@ import com.hivemq.configuration.entity.combining.DataCombiningSourcesEntity; import com.hivemq.configuration.entity.combining.DataIdentifierReferenceEntity; import com.hivemq.configuration.entity.combining.EntityReferenceEntity; +import com.hivemq.configuration.entity.listener.TCPListenerEntity; +import com.hivemq.configuration.entity.listener.TlsTCPListenerEntity; +import com.hivemq.configuration.entity.listener.WebsocketListenerEntity; +import com.hivemq.configuration.entity.listener.TlsWebsocketListenerEntity; +import com.hivemq.configuration.entity.listener.UDPListenerEntity; +import com.hivemq.configuration.entity.listener.UDPBroadcastListenerEntity; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.SchemaOutputResolver; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; -import java.io.StringWriter; -import java.nio.file.Files; /** * Utility class to generate XSD schema from JAXB-annotated configuration entity classes. *

- * This generates the structural XSD from the Java annotations and adds custom simple types - * for value constraints that JAXB cannot express. + * This generates the structural XSD from the Java annotations and post-processes it + * to ensure backwards compatibility with existing configuration files. + *

+ * Post-processing includes: + *

    + *
  • Replacing xs:sequence with xs:all for flexible element ordering
  • + *
  • Adding proper listener type choices (tcp-listener, tls-tcp-listener, etc.)
  • + *
  • Adding xs:any for protocol-adapters to support legacy adapter configs
  • + *
  • Adding custom simple types for value constraints
  • + *
*

* Usage: Run the main method with an optional output file path as argument. * If no argument is provided, outputs to build/generated-xsd/config-generated.xsd */ public class GenSchemaMain { + private static final String XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema"; + /** * All classes that need to be included in the schema generation. - * This includes the root entity and all referenced entities that use @XmlRootElement. */ private static final Class[] SCHEMA_CLASSES = { HiveMQConfigEntity.class, - // Data combiner entities (for full XSD compliance) + // Data combiner entities DataCombinerEntity.class, DataCombiningEntity.class, DataCombiningSourcesEntity.class, DataCombiningDestinationEntity.class, DataIdentifierReferenceEntity.class, EntityReferenceEntity.class, - InstructionEntity.class + InstructionEntity.class, + // MQTT Listener entities + TCPListenerEntity.class, + TlsTCPListenerEntity.class, + WebsocketListenerEntity.class, + TlsWebsocketListenerEntity.class, + // MQTT-SN Listener entities + UDPListenerEntity.class, + UDPBroadcastListenerEntity.class }; - /** - * Custom simple types to add to the generated schema. - * These provide value constraints that JAXB cannot express through annotations. - */ - private static final String CUSTOM_SIMPLE_TYPES = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - """; - public static void main(String[] args) throws Exception { String outputPath = args.length > 0 ? args[0] : "build/generated-xsd/config-generated.xsd"; File outputFile = new File(outputPath); - // Ensure parent directory exists File parentDir = outputFile.getParentFile(); if (parentDir != null && !parentDir.exists()) { if (!parentDir.mkdirs()) { @@ -145,52 +109,369 @@ public static void main(String[] args) throws Exception { } /** - * Generates the XSD schema to the specified file. - * - * @param outputFile the file to write the schema to - * @throws JAXBException if JAXB context creation fails - * @throws IOException if file writing fails + * Generates the XSD schema to the specified file with all post-processing applied. */ - public static void generateSchema(File outputFile) throws JAXBException, IOException { - // Generate to a temporary file first + public static void generateSchema(File outputFile) throws Exception { + // Step 1: Generate base schema from JAXB File tempFile = File.createTempFile("schema", ".xsd"); tempFile.deleteOnExit(); + generateBaseSchema(tempFile); - JAXBContext context = JAXBContext.newInstance(SCHEMA_CLASSES); + // Step 2: Load and post-process the schema + Document doc = loadXmlDocument(tempFile); + postProcessSchema(doc); + + // Step 3: Write the final schema + writeXmlDocument(doc, outputFile); + } + private static void generateBaseSchema(File outputFile) throws JAXBException, IOException { + JAXBContext context = JAXBContext.newInstance(SCHEMA_CLASSES); context.generateSchema(new SchemaOutputResolver() { @Override public Result createOutput(String namespaceUri, String suggestedFileName) throws IOException { - StreamResult result = new StreamResult(tempFile); - result.setSystemId(tempFile.toURI().toString()); + StreamResult result = new StreamResult(outputFile); + result.setSystemId(outputFile.toURI().toString()); return result; } }); + } + + private static Document loadXmlDocument(File file) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(file); + } + + private static void writeXmlDocument(Document doc, File outputFile) throws Exception { + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + transformer.transform(new DOMSource(doc), new StreamResult(outputFile)); + } + + /** + * Applies all post-processing transformations to make the schema backwards compatible. + */ + private static void postProcessSchema(Document doc) { + replaceSequenceWithAllForRootEntity(doc); + makeConfigVersionOptional(doc); + fixMqttListeners(doc); + fixMqttSnListeners(doc); + fixProtocolAdapters(doc); + fixModules(doc); + addCustomSimpleTypes(doc); + } + + /** + * Makes the config-version element optional (minOccurs="0"). + */ + private static void makeConfigVersionOptional(Document doc) { + Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + if (complexType == null) return; + + Element configVersionElement = findChildElementByName(complexType, "config-version"); + if (configVersionElement != null) { + configVersionElement.setAttribute("minOccurs", "0"); + } + } + + /** + * Replaces xs:sequence with xs:all in hiveMQConfigEntity to allow elements in any order. + */ + private static void replaceSequenceWithAllForRootEntity(Document doc) { + Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + if (complexType == null) { + System.err.println("Warning: Could not find hiveMQConfigEntity complexType"); + return; + } + + // Find the xs:sequence child + NodeList children = complexType.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element && "sequence".equals(child.getLocalName())) { + // Create new xs:all element + Element allElement = doc.createElementNS(XS_NAMESPACE, "xs:all"); + + // Move all children from sequence to all + NodeList sequenceChildren = child.getChildNodes(); + while (sequenceChildren.getLength() > 0) { + Node seqChild = sequenceChildren.item(0); + allElement.appendChild(seqChild); + } + + // Replace sequence with all + complexType.replaceChild(allElement, child); + break; + } + } + } + + /** + * Fixes mqtt-listeners to use xs:choice with all listener types. + */ + private static void fixMqttListeners(Document doc) { + fixListenerElement(doc, "mqtt-listeners", new String[]{ + "tcp-listener", "tls-tcp-listener", "websocket-listener", "tls-websocket-listener" + }); + } + + /** + * Fixes mqtt-sn-listeners to use xs:choice with appropriate listener types. + */ + private static void fixMqttSnListeners(Document doc) { + fixListenerElement(doc, "mqtt-sn-listeners", new String[]{ + "udp-listener", "udp-broadcast-listener" + }); + } + + /** + * Generic method to fix listener wrapper elements with proper xs:choice. + */ + private static void fixListenerElement(Document doc, String elementName, String[] listenerTypes) { + // Find the element in hiveMQConfigEntity + Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + if (complexType == null) return; + + Element listenersElement = findChildElementByName(complexType, elementName); + if (listenersElement == null) return; + + // Find the inner complexType + Element innerComplexType = findFirstChildElement(listenersElement, "complexType"); + if (innerComplexType == null) return; + + // Replace the content with xs:choice + // Remove existing children (xs:sequence with xs:element ref="address") + while (innerComplexType.hasChildNodes()) { + innerComplexType.removeChild(innerComplexType.getFirstChild()); + } + + // Create xs:choice with all listener types + Element choice = doc.createElementNS(XS_NAMESPACE, "xs:choice"); + choice.setAttribute("minOccurs", "0"); + choice.setAttribute("maxOccurs", "unbounded"); + + for (String listenerType : listenerTypes) { + Element elementRef = doc.createElementNS(XS_NAMESPACE, "xs:element"); + elementRef.setAttribute("ref", listenerType); + choice.appendChild(elementRef); + } + + innerComplexType.appendChild(choice); + } + + /** + * Fixes protocol-adapters to use xs:any for both new and legacy adapter formats. + * Using xs:any alone avoids non-determinism issues that occur when mixing + * named elements with xs:any in a choice. + */ + private static void fixProtocolAdapters(Document doc) { + Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + if (complexType == null) return; - // Post-process to add custom simple types - addCustomSimpleTypes(tempFile, outputFile); + Element adaptersElement = findChildElementByName(complexType, "protocol-adapters"); + if (adaptersElement == null) return; + + Element innerComplexType = findFirstChildElement(adaptersElement, "complexType"); + if (innerComplexType == null) return; + + // Clear existing content + while (innerComplexType.hasChildNodes()) { + innerComplexType.removeChild(innerComplexType.getFirstChild()); + } + + // Create xs:sequence with xs:any (skip validation for all adapter elements) + // This allows both and legacy adapter-specific elements like + Element sequence = doc.createElementNS(XS_NAMESPACE, "xs:sequence"); + + Element choice = doc.createElementNS(XS_NAMESPACE, "xs:choice"); + choice.setAttribute("minOccurs", "0"); + choice.setAttribute("maxOccurs", "unbounded"); + + // Use xs:any alone to avoid non-determinism + Element anyElement = doc.createElementNS(XS_NAMESPACE, "xs:any"); + anyElement.setAttribute("processContents", "skip"); + choice.appendChild(anyElement); + + sequence.appendChild(choice); + innerComplexType.appendChild(sequence); + } + + /** + * Fixes modules element to use xs:any for arbitrary module configurations. + */ + private static void fixModules(Document doc) { + Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + if (complexType == null) return; + + Element modulesElement = findChildElementByName(complexType, "modules"); + if (modulesElement == null) return; + + // Remove the type attribute and add inline complexType with xs:any + modulesElement.removeAttribute("type"); + + // Create inline complexType + Element innerComplexType = doc.createElementNS(XS_NAMESPACE, "xs:complexType"); + Element sequence = doc.createElementNS(XS_NAMESPACE, "xs:sequence"); + + Element anyElement = doc.createElementNS(XS_NAMESPACE, "xs:any"); + anyElement.setAttribute("processContents", "skip"); + anyElement.setAttribute("minOccurs", "0"); + anyElement.setAttribute("maxOccurs", "unbounded"); + + sequence.appendChild(anyElement); + innerComplexType.appendChild(sequence); + modulesElement.appendChild(innerComplexType); } /** - * Adds custom simple types to the generated XSD by inserting them before the closing schema tag. - * - * @param inputFile the generated XSD file - * @param outputFile the final output file with custom types added - * @throws IOException if file operations fail + * Adds custom simple types for value constraints (only if they don't already exist). */ - private static void addCustomSimpleTypes(File inputFile, File outputFile) throws IOException { - String content = Files.readString(inputFile.toPath()); + private static void addCustomSimpleTypes(Document doc) { + Element schemaElement = doc.getDocumentElement(); + + addSimpleTypeIfNotExists(doc, schemaElement, "port", "xs:int", + new String[]{"minInclusive", "0"}, new String[]{"maxInclusive", "65535"}); + + addSimpleTypeIfNotExists(doc, schemaElement, "nonEmptyString", "xs:string", + new String[]{"minLength", "1"}, new String[]{"whiteSpace", "collapse"}); + + addSimpleTypeIfNotExists(doc, schemaElement, "uuidType", "xs:string", + new String[]{"pattern", "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"}); + + // clientAuthenticationMode is often generated by TLS entities, only add if missing + addSimpleTypeWithEnumerationIfNotExists(doc, schemaElement, "clientAuthenticationMode", "xs:string", + "OPTIONAL", "REQUIRED", "NONE"); + + addSimpleTypeIfNotExists(doc, schemaElement, "qosType", "xs:int", + new String[]{"minInclusive", "0"}, new String[]{"maxInclusive", "2"}); + + addSimpleTypeIfNotExists(doc, schemaElement, "positiveInteger", "xs:int", + new String[]{"minInclusive", "1"}); + + addSimpleTypeIfNotExists(doc, schemaElement, "nonNegativeInteger", "xs:int", + new String[]{"minInclusive", "0"}); - // Find the closing tag and insert custom types before it - int closingTagIndex = content.lastIndexOf(""); - if (closingTagIndex == -1) { - throw new IOException("Could not find closing tag in generated XSD"); + addSimpleTypeIfNotExists(doc, schemaElement, "nonNegativeLong", "xs:long", + new String[]{"minInclusive", "0"}); + } + + private static boolean simpleTypeExists(Document doc, String name) { + NodeList simpleTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "simpleType"); + for (int i = 0; i < simpleTypes.getLength(); i++) { + Element st = (Element) simpleTypes.item(i); + if (name.equals(st.getAttribute("name"))) { + return true; + } } + return false; + } - String modifiedContent = content.substring(0, closingTagIndex) + - CUSTOM_SIMPLE_TYPES + - content.substring(closingTagIndex); + private static void addSimpleTypeIfNotExists(Document doc, Element parent, String name, String baseType, String[]... facets) { + if (simpleTypeExists(doc, name)) { + return; + } + addSimpleType(doc, parent, name, baseType, facets); + } - Files.writeString(outputFile.toPath(), modifiedContent); + private static void addSimpleTypeWithEnumerationIfNotExists(Document doc, Element parent, String name, String baseType, String... values) { + if (simpleTypeExists(doc, name)) { + return; + } + addSimpleTypeWithEnumeration(doc, parent, name, baseType, values); + } + + private static void addSimpleType(Document doc, Element parent, String name, String baseType, String[]... facets) { + Element simpleType = doc.createElementNS(XS_NAMESPACE, "xs:simpleType"); + simpleType.setAttribute("name", name); + + Element restriction = doc.createElementNS(XS_NAMESPACE, "xs:restriction"); + restriction.setAttribute("base", baseType); + + for (String[] facet : facets) { + Element facetElement = doc.createElementNS(XS_NAMESPACE, "xs:" + facet[0]); + facetElement.setAttribute("value", facet[1]); + restriction.appendChild(facetElement); + } + + simpleType.appendChild(restriction); + parent.appendChild(simpleType); + } + + private static void addSimpleTypeWithEnumeration(Document doc, Element parent, String name, String baseType, String... values) { + Element simpleType = doc.createElementNS(XS_NAMESPACE, "xs:simpleType"); + simpleType.setAttribute("name", name); + + Element restriction = doc.createElementNS(XS_NAMESPACE, "xs:restriction"); + restriction.setAttribute("base", baseType); + + for (String value : values) { + Element enumElement = doc.createElementNS(XS_NAMESPACE, "xs:enumeration"); + enumElement.setAttribute("value", value); + restriction.appendChild(enumElement); + } + + simpleType.appendChild(restriction); + parent.appendChild(simpleType); + } + + // Helper methods for DOM traversal + + private static Element findComplexTypeByName(Document doc, String name) { + NodeList complexTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "complexType"); + for (int i = 0; i < complexTypes.getLength(); i++) { + Element ct = (Element) complexTypes.item(i); + if (name.equals(ct.getAttribute("name"))) { + return ct; + } + } + return null; + } + + private static Element findChildElementByName(Element parent, String elementName) { + // Search through xs:all or xs:sequence children + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element) { + Element childElement = (Element) child; + String localName = childElement.getLocalName(); + if ("all".equals(localName) || "sequence".equals(localName)) { + // Search within all/sequence + Element found = findElementWithNameAttribute(childElement, elementName); + if (found != null) return found; + } + } + } + return null; + } + + private static Element findElementWithNameAttribute(Element parent, String nameValue) { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element) { + Element childElement = (Element) child; + if ("element".equals(childElement.getLocalName()) && + nameValue.equals(childElement.getAttribute("name"))) { + return childElement; + } + } + } + return null; + } + + private static Element findFirstChildElement(Element parent, String localName) { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element && localName.equals(child.getLocalName())) { + return (Element) child; + } + } + return null; } } From ba4840eab316a4d705f628fb6ef6503183442acf Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Mon, 1 Dec 2025 20:35:49 +0100 Subject: [PATCH 03/10] Round 3: Generating fully compatible XSD --- .../hivemq/configuration/GenSchemaMain.java | 102 +++++++++++++++++- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index 1364145092..b2436506a0 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -162,6 +162,8 @@ private static void postProcessSchema(Document doc) { fixMqttSnListeners(doc); fixProtocolAdapters(doc); fixModules(doc); + fixDataCombinerEntity(doc); + fixEmptyElementTypes(doc); addCustomSimpleTypes(doc); } @@ -327,6 +329,96 @@ private static void fixModules(Document doc) { modulesElement.appendChild(innerComplexType); } + /** + * Fixes dataCombinerEntity and dataCombiningEntity to use xs:all instead of xs:sequence + * for flexible element ordering, and makes wrapper elements optional. + */ + private static void fixDataCombinerEntity(Document doc) { + replaceSequenceWithAll(doc, "dataCombinerEntity"); + replaceSequenceWithAll(doc, "dataCombiningEntity"); + + // Make wrapper elements optional in dataCombinerEntity + makeElementOptionalInType(doc, "dataCombinerEntity", "entity-references"); + makeElementOptionalInType(doc, "dataCombinerEntity", "data-combinings"); + + // Make sources optional in dataCombiningEntity + makeElementOptionalInType(doc, "dataCombiningEntity", "sources"); + } + + /** + * Makes a specific element optional (minOccurs="0") within a complex type. + */ + private static void makeElementOptionalInType(Document doc, String typeName, String elementName) { + Element complexType = findComplexTypeByName(doc, typeName); + if (complexType == null) return; + + Element element = findChildElementByName(complexType, elementName); + if (element != null) { + element.setAttribute("minOccurs", "0"); + } + } + + /** + * Replaces xs:sequence with xs:all in the specified complex type. + */ + private static void replaceSequenceWithAll(Document doc, String typeName) { + Element complexType = findComplexTypeByName(doc, typeName); + if (complexType == null) return; + + NodeList children = complexType.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child instanceof Element && "sequence".equals(child.getLocalName())) { + Element allElement = doc.createElementNS(XS_NAMESPACE, "xs:all"); + + // Move all children from sequence to all + NodeList sequenceChildren = child.getChildNodes(); + while (sequenceChildren.getLength() > 0) { + Node seqChild = sequenceChildren.item(0); + allElement.appendChild(seqChild); + } + + complexType.replaceChild(allElement, child); + break; + } + } + } + + /** + * Fixes complex types that can appear as empty elements by making all children optional. + * This allows configurations like {@code } or {@code } to validate. + */ + private static void fixEmptyElementTypes(Document doc) { + String[] typesToFix = { + "mqttSnConfigEntity", + "adminApiEntity", + "dynamicConfigEntity", + "usageTrackingConfigEntity", + // Base types that are inherited by types above + "enabledEntity" + }; + + for (String typeName : typesToFix) { + makeAllChildrenOptional(doc, typeName); + } + } + + /** + * Makes all child elements optional (minOccurs="0") in the specified complex type. + */ + private static void makeAllChildrenOptional(Document doc, String typeName) { + Element complexType = findComplexTypeByName(doc, typeName); + if (complexType == null) return; + + NodeList elements = complexType.getElementsByTagNameNS(XS_NAMESPACE, "element"); + for (int i = 0; i < elements.getLength(); i++) { + Element element = (Element) elements.item(i); + if (!element.hasAttribute("minOccurs")) { + element.setAttribute("minOccurs", "0"); + } + } + } + /** * Adds custom simple types for value constraints (only if they don't already exist). */ @@ -455,9 +547,13 @@ private static Element findElementWithNameAttribute(Element parent, String nameV Node child = children.item(i); if (child instanceof Element) { Element childElement = (Element) child; - if ("element".equals(childElement.getLocalName()) && - nameValue.equals(childElement.getAttribute("name"))) { - return childElement; + if ("element".equals(childElement.getLocalName())) { + // Check both "name" attribute and "ref" attribute + String name = childElement.getAttribute("name"); + String ref = childElement.getAttribute("ref"); + if (nameValue.equals(name) || nameValue.equals(ref)) { + return childElement; + } } } } From ab0aefb057a5c245af235d93242d7c721a9ece72 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Mon, 1 Dec 2025 20:54:39 +0100 Subject: [PATCH 04/10] Round 4: Make generated XSD the default --- hivemq-edge/build.gradle.kts | 13 +- hivemq-edge/src/main/resources/config.xsd | 1314 --------------------- 2 files changed, 10 insertions(+), 1317 deletions(-) delete mode 100644 hivemq-edge/src/main/resources/config.xsd diff --git a/hivemq-edge/build.gradle.kts b/hivemq-edge/build.gradle.kts index dc86849750..70b3f0694c 100644 --- a/hivemq-edge/build.gradle.kts +++ b/hivemq-edge/build.gradle.kts @@ -617,12 +617,19 @@ val generateXsd by tasks.registering(JavaExec::class) { mainClass.set("com.hivemq.configuration.GenSchemaMain") classpath = sourceSets.test.get().runtimeClasspath - val outputFile = layout.buildDirectory.file("generated-xsd/config-generated.xsd") + val outputDir = layout.buildDirectory.dir("generated-resources/xsd") + val outputFile = outputDir.map { it.file("config.xsd") } args(outputFile.get().asFile.absolutePath) - outputs.file(outputFile) + outputs.dir(outputDir) doFirst { - outputFile.get().asFile.parentFile.mkdirs() + outputDir.get().asFile.mkdirs() } } + +// Include the generated XSD in the jar (runs after test compilation) +tasks.jar { + dependsOn(generateXsd) + from(generateXsd.map { it.outputs.files }) +} diff --git a/hivemq-edge/src/main/resources/config.xsd b/hivemq-edge/src/main/resources/config.xsd deleted file mode 100644 index 557011644c..0000000000 --- a/hivemq-edge/src/main/resources/config.xsd +++ /dev/null @@ -1,1314 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Default: in-memory - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - Default: /mqtt - - - - - - - - - - - - Default: false - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - Default: /mqtt - - - - - - - - - - - - Default: false - - - - - - - - - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Timeout in milliseconds. Default: 10000 - - - - - Default: NONE - - - - - - - - - - - - - - Default: 1000 - - - - - - - - - - Default: discard - - - - - - - - - - - - - - - - - Default: true - - - - - - - - - - - Default: true - - - - - - - - - - - Default: 2 - - - - - - - - - - - - - - - - - - Default: true - - - - - Default: 5 - - - - - - - - - - - - - - - - - Interval in seconds. Default: 4294967296 - - - - - - - - - - - - - - - - - Interval in seconds. Default: 4294967296 - - - - - - - - - - - - - - - - - Default: true - - - - - - - - - - - Default: true - - - - - - - - - - - - Keep-alive in seconds. Default: 65535 - - - - - - - - - - Default: true - - - - - - - - - - - - Size in bytes. Default: 268435460 - - - - - - - - - - - - - - - - Default: 10 - - - - - - - - - - - - - - - - - - - - Default: 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default: false - - - - - - - - - - - Default: false - - - - - - - - - - - Default: false - - - - - - - - - - - Default: false - - - - - - - - - Size in bytes. Default: 23 - - - - - - - - - - - - - Default: false - - - - - Default: 65535 - - - - - - - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - - - - - - - - - - - - - - - - -1 means unlimited. License limitations still apply. Default: -1 - - - - - - - - - - - Default: 65535 - - - - - - - - - - - Default: 65535 - - - - - - - - - - - Timeout in milliseconds. Default: 10000 - - - - - - - - - - Throttling in bytes per seconds. Default: 0 - - - - - - - - - - - - - - - - - - - Default: false - - - - - - - - - - - Default: true - - - - - - - - - - - Default: true - - - - - - - - - - - Default: true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default: 1883 - - - - - - - - - - - - - - - - - - - Session Expiry Interval in seconds. - Default: 3600 - - - - - - - - - - - - - Keep-alive in seconds. Default: 60 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - TLS Handshake timeout in seconds. Default: - 10. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default: 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default: 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Limit of maximum bridge hops until a message is not - forwarded anymore. - Default: 1 - - - - - - - - - - - - - - - - - - - - - - - - - - Default: true - - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - - - - - - Default: 0.0.0.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LDAP server port. If not specified, defaults to 389 for NONE/START_TLS or 636 for LDAPS. - - - - - - - - - - - TLS mode for LDAP connection. Default: NONE - - - - - - - - - - - - - - - Path to truststore file. If not specified, system default CA certificates are used. - - - - - Password for the truststore. - - - - - Type of truststore (e.g., JKS, PKCS12). - - - - - - - - - - - - - - - - Connection timeout in milliseconds. 0 means use SDK default. - - - - - Response timeout in milliseconds. 0 means use SDK default. - - - - - Timeout for search requests. - - - - - Max number of connections open in the connection pool. - - - - - Attribute for the user id. Example: uid - - - - - BaseDN fo searches and tempalted requests. - - - - - An optional object class to check for retrieved entries. - - - - - Enable directory descent instead of using direct binding. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 5bd95f27868fa2deb3666d319aa499cb3186d15a Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Mon, 1 Dec 2025 21:30:18 +0100 Subject: [PATCH 05/10] Round 5: Added extensive JavaDoc to docuemnt the reason for each post processing step --- .../hivemq/configuration/GenSchemaMain.java | 131 +++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index b2436506a0..41fb18feab 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -169,6 +169,16 @@ private static void postProcessSchema(Document doc) { /** * Makes the config-version element optional (minOccurs="0"). + *

+ * Why this is needed: + * The {@code config-version} element has a default value in the Java entity, but JAXB generates + * schema elements as required by default. Existing config files may omit this element entirely. + *

+ * Why JAXB cannot express this directly: + * JAXB's {@code @XmlElement(required = false)} only affects marshalling behavior (whether null + * values are written), not schema generation. JAXB always generates elements without + * {@code minOccurs="0"} unless they are part of a collection. There is no annotation to + * explicitly set {@code minOccurs} in the generated schema. */ private static void makeConfigVersionOptional(Document doc) { Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); @@ -182,6 +192,26 @@ private static void makeConfigVersionOptional(Document doc) { /** * Replaces xs:sequence with xs:all in hiveMQConfigEntity to allow elements in any order. + *

+ * Why this is needed: + * JAXB always generates {@code xs:sequence} for complex types, which requires XML elements + * to appear in a specific order matching the field declaration order in the Java class. + * However, existing config files have elements in arbitrary order, so we need {@code xs:all} + * which allows elements in any order. + *

+ * Why JAXB cannot express this directly: + *

    + *
  • There is no JAXB annotation to generate {@code xs:all} instead of {@code xs:sequence}. + * {@code @XmlType(propOrder = {...})} controls element order within a sequence but + * cannot switch to {@code xs:all}.
  • + *
  • JAXB was designed primarily for marshalling/unmarshalling Java objects, not for + * schema-first design. The Java object model naturally maps to sequences (ordered fields).
  • + *
  • In XSD 1.0, {@code xs:all} has restrictions (each element can appear at most once, + * cannot be nested within other model groups) that make it less suitable for JAXB's + * general-purpose schema generation.
  • + *
+ *

+ * Post-processing the generated XSD is the standard workaround for this JAXB limitation. */ private static void replaceSequenceWithAllForRootEntity(Document doc) { Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); @@ -214,6 +244,19 @@ private static void replaceSequenceWithAllForRootEntity(Document doc) { /** * Fixes mqtt-listeners to use xs:choice with all listener types. + *

+ * Why this is needed: + * The mqtt-listeners wrapper contains polymorphic listener elements (tcp-listener, tls-tcp-listener, + * websocket-listener, tls-websocket-listener). The schema needs {@code xs:choice} to allow any of + * these element types in any order and quantity. + *

+ * Why JAXB cannot express this directly: + * The Java entity uses {@code @XmlElementRef} on a {@code List} where + * {@code ListenerEntity} is a base class. JAXB generates a reference to an abstract type or + * a single element reference, not an {@code xs:choice} with all concrete subtypes. JAXB's + * {@code @XmlElements} annotation can list multiple types but generates {@code xs:choice} + * only at the element level, not properly handling the inheritance hierarchy with + * {@code @XmlElementWrapper}. */ private static void fixMqttListeners(Document doc) { fixListenerElement(doc, "mqtt-listeners", new String[]{ @@ -223,6 +266,8 @@ private static void fixMqttListeners(Document doc) { /** * Fixes mqtt-sn-listeners to use xs:choice with appropriate listener types. + *

+ * See {@link #fixMqttListeners(Document)} for explanation of why this post-processing is needed. */ private static void fixMqttSnListeners(Document doc) { fixListenerElement(doc, "mqtt-sn-listeners", new String[]{ @@ -232,6 +277,10 @@ private static void fixMqttSnListeners(Document doc) { /** * Generic method to fix listener wrapper elements with proper xs:choice. + * + * @param doc the XSD document to modify + * @param elementName the wrapper element name (e.g., "mqtt-listeners") + * @param listenerTypes the concrete listener element names to include in the choice */ private static void fixListenerElement(Document doc, String elementName, String[] listenerTypes) { // Find the element in hiveMQConfigEntity @@ -267,8 +316,25 @@ private static void fixListenerElement(Document doc, String elementName, String[ /** * Fixes protocol-adapters to use xs:any for both new and legacy adapter formats. - * Using xs:any alone avoids non-determinism issues that occur when mixing - * named elements with xs:any in a choice. + *

+ * Why this is needed: + * Protocol adapters can be configured in two ways: the new format uses {@code } + * elements, while legacy configs use adapter-specific element names like {@code }, + * {@code }, etc. The schema must accept any element within protocol-adapters. + *

+ * Why JAXB cannot express this directly: + *

    + *
  • JAXB has {@code @XmlAnyElement} which generates {@code xs:any}, but it cannot be + * combined with typed elements in the same collection.
  • + *
  • The Java entity uses {@code @XmlElement(name = "protocol-adapter")} for the new format, + * which generates a specific element reference, not a wildcard.
  • + *
  • To support both formats, we need {@code xs:any processContents="skip"} which tells + * the validator to accept any element without validation - this cannot be expressed + * through JAXB annotations while keeping the typed Java binding.
  • + *
+ *

+ * Note: Using {@code xs:any} alone avoids non-determinism issues that occur when mixing + * named elements with {@code xs:any} in a choice. */ private static void fixProtocolAdapters(Document doc) { Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); @@ -304,6 +370,18 @@ private static void fixProtocolAdapters(Document doc) { /** * Fixes modules element to use xs:any for arbitrary module configurations. + *

+ * Why this is needed: + * The modules element contains arbitrary configuration for dynamically loaded modules. + * Each module can define its own XML structure, so the schema cannot know the element names + * or structure in advance. + *

+ * Why JAXB cannot express this directly: + * The Java entity uses {@code @XmlJavaTypeAdapter(ArbitraryValuesMapAdapter.class)} to handle + * the dynamic content as a {@code Map}. JAXB generates a reference to the + * adapter's mapped type, not {@code xs:any}. While {@code @XmlAnyElement} exists, it requires + * the field to be {@code List} or similar DOM types, which would change the Java API. + * The adapter approach provides a cleaner Java API but requires schema post-processing. */ private static void fixModules(Document doc) { Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); @@ -332,6 +410,17 @@ private static void fixModules(Document doc) { /** * Fixes dataCombinerEntity and dataCombiningEntity to use xs:all instead of xs:sequence * for flexible element ordering, and makes wrapper elements optional. + *

+ * Why this is needed: + * Like the root entity, data combiner configurations may have elements in any order in + * existing config files. Additionally, wrapper elements like {@code entity-references} and + * {@code data-combinings} should be optional when empty. + *

+ * Why JAXB cannot express this directly: + * See {@link #replaceSequenceWithAllForRootEntity(Document)} for the explanation of why + * {@code xs:all} cannot be generated by JAXB. For the optional wrapper elements, JAXB's + * {@code @XmlElementWrapper} does not support a {@code required} attribute - wrappers are + * always generated as required in the schema even when the collection can be empty. */ private static void fixDataCombinerEntity(Document doc) { replaceSequenceWithAll(doc, "dataCombinerEntity"); @@ -347,6 +436,10 @@ private static void fixDataCombinerEntity(Document doc) { /** * Makes a specific element optional (minOccurs="0") within a complex type. + * + * @param doc the XSD document to modify + * @param typeName the name of the complex type containing the element + * @param elementName the name of the element to make optional */ private static void makeElementOptionalInType(Document doc, String typeName, String elementName) { Element complexType = findComplexTypeByName(doc, typeName); @@ -360,6 +453,11 @@ private static void makeElementOptionalInType(Document doc, String typeName, Str /** * Replaces xs:sequence with xs:all in the specified complex type. + *

+ * See {@link #replaceSequenceWithAllForRootEntity(Document)} for explanation of why this is needed. + * + * @param doc the XSD document to modify + * @param typeName the name of the complex type to modify */ private static void replaceSequenceWithAll(Document doc, String typeName) { Element complexType = findComplexTypeByName(doc, typeName); @@ -386,7 +484,19 @@ private static void replaceSequenceWithAll(Document doc, String typeName) { /** * Fixes complex types that can appear as empty elements by making all children optional. - * This allows configurations like {@code } or {@code } to validate. + *

+ * Why this is needed: + * Some configuration sections can be specified as empty self-closing elements like + * {@code } or {@code } to use all default values. The schema must + * allow these elements to have no children. + *

+ * Why JAXB cannot express this directly: + * JAXB generates child elements as required by default. While {@code @XmlElement(required = false)} + * exists, it only affects marshalling behavior (whether to write null values), not the + * {@code minOccurs} attribute in the generated schema. There is no JAXB annotation to set + * {@code minOccurs="0"} on generated elements. Additionally, for types using inheritance + * (like {@code adminApiEntity} extending {@code enabledEntity}), the base type's elements + * also need to be made optional, which requires modifying multiple generated complex types. */ private static void fixEmptyElementTypes(Document doc) { String[] typesToFix = { @@ -405,6 +515,9 @@ private static void fixEmptyElementTypes(Document doc) { /** * Makes all child elements optional (minOccurs="0") in the specified complex type. + * + * @param doc the XSD document to modify + * @param typeName the name of the complex type whose children should be made optional */ private static void makeAllChildrenOptional(Document doc, String typeName) { Element complexType = findComplexTypeByName(doc, typeName); @@ -421,6 +534,18 @@ private static void makeAllChildrenOptional(Document doc, String typeName) { /** * Adds custom simple types for value constraints (only if they don't already exist). + *

+ * Why this is needed: + * XSD simple types with restrictions (like port numbers 0-65535, non-empty strings, UUIDs) + * provide better validation and documentation than plain {@code xs:string} or {@code xs:int}. + *

+ * Why JAXB cannot express this directly: + * JAXB maps Java types directly to XSD built-in types (String → xs:string, int → xs:int). + * There is no annotation to specify XSD facets like {@code minInclusive}, {@code maxInclusive}, + * {@code pattern}, or {@code minLength}. While custom {@code XmlAdapter} implementations can + * transform values during marshalling/unmarshalling, they do not affect schema generation. + * Bean Validation annotations (like {@code @Min}, {@code @Max}, {@code @Pattern}) are also + * not translated to XSD constraints by JAXB. */ private static void addCustomSimpleTypes(Document doc) { Element schemaElement = doc.getDocumentElement(); From da89a622248db133884f528ac8dc5791b71ec2b0 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Tue, 2 Dec 2025 08:44:33 +0100 Subject: [PATCH 06/10] Cleanup --- .../hivemq/configuration/GenSchemaMain.java | 216 +++++++++--------- 1 file changed, 105 insertions(+), 111 deletions(-) diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index 41fb18feab..49ded3b4be 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -26,23 +26,19 @@ import com.hivemq.configuration.entity.combining.EntityReferenceEntity; import com.hivemq.configuration.entity.listener.TCPListenerEntity; import com.hivemq.configuration.entity.listener.TlsTCPListenerEntity; -import com.hivemq.configuration.entity.listener.WebsocketListenerEntity; import com.hivemq.configuration.entity.listener.TlsWebsocketListenerEntity; -import com.hivemq.configuration.entity.listener.UDPListenerEntity; import com.hivemq.configuration.entity.listener.UDPBroadcastListenerEntity; +import com.hivemq.configuration.entity.listener.UDPListenerEntity; +import com.hivemq.configuration.entity.listener.WebsocketListenerEntity; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.SchemaOutputResolver; import org.w3c.dom.Document; import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; -import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; @@ -93,11 +89,11 @@ public class GenSchemaMain { UDPBroadcastListenerEntity.class }; - public static void main(String[] args) throws Exception { - String outputPath = args.length > 0 ? args[0] : "build/generated-xsd/config-generated.xsd"; - File outputFile = new File(outputPath); + public static void main(final String[] args) throws Exception { + final var outputPath = args.length > 0 ? args[0] : "build/generated-xsd/config-generated.xsd"; + final var outputFile = new File(outputPath); - File parentDir = outputFile.getParentFile(); + final var parentDir = outputFile.getParentFile(); if (parentDir != null && !parentDir.exists()) { if (!parentDir.mkdirs()) { throw new IOException("Failed to create directory: " + parentDir.getAbsolutePath()); @@ -111,42 +107,42 @@ public static void main(String[] args) throws Exception { /** * Generates the XSD schema to the specified file with all post-processing applied. */ - public static void generateSchema(File outputFile) throws Exception { + public static void generateSchema(final File outputFile) throws Exception { // Step 1: Generate base schema from JAXB - File tempFile = File.createTempFile("schema", ".xsd"); + final var tempFile = File.createTempFile("schema", ".xsd"); tempFile.deleteOnExit(); generateBaseSchema(tempFile); // Step 2: Load and post-process the schema - Document doc = loadXmlDocument(tempFile); + final var doc = loadXmlDocument(tempFile); postProcessSchema(doc); // Step 3: Write the final schema writeXmlDocument(doc, outputFile); } - private static void generateBaseSchema(File outputFile) throws JAXBException, IOException { - JAXBContext context = JAXBContext.newInstance(SCHEMA_CLASSES); + private static void generateBaseSchema(final File outputFile) throws JAXBException, IOException { + final var context = JAXBContext.newInstance(SCHEMA_CLASSES); context.generateSchema(new SchemaOutputResolver() { @Override - public Result createOutput(String namespaceUri, String suggestedFileName) throws IOException { - StreamResult result = new StreamResult(outputFile); + public Result createOutput(final String namespaceUri, final String suggestedFileName) { + final var result = new StreamResult(outputFile); result.setSystemId(outputFile.toURI().toString()); return result; } }); } - private static Document loadXmlDocument(File file) throws Exception { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + private static Document loadXmlDocument(final File file) throws Exception { + final var factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); + final var builder = factory.newDocumentBuilder(); return builder.parse(file); } - private static void writeXmlDocument(Document doc, File outputFile) throws Exception { - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - Transformer transformer = transformerFactory.newTransformer(); + private static void writeXmlDocument(final Document doc, final File outputFile) throws Exception { + final var transformerFactory = TransformerFactory.newInstance(); + final var transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); transformer.transform(new DOMSource(doc), new StreamResult(outputFile)); @@ -155,7 +151,7 @@ private static void writeXmlDocument(Document doc, File outputFile) throws Excep /** * Applies all post-processing transformations to make the schema backwards compatible. */ - private static void postProcessSchema(Document doc) { + private static void postProcessSchema(final Document doc) { replaceSequenceWithAllForRootEntity(doc); makeConfigVersionOptional(doc); fixMqttListeners(doc); @@ -180,11 +176,11 @@ private static void postProcessSchema(Document doc) { * {@code minOccurs="0"} unless they are part of a collection. There is no annotation to * explicitly set {@code minOccurs} in the generated schema. */ - private static void makeConfigVersionOptional(Document doc) { - Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + private static void makeConfigVersionOptional(final Document doc) { + final var complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); if (complexType == null) return; - Element configVersionElement = findChildElementByName(complexType, "config-version"); + final var configVersionElement = findChildElementByName(complexType, "config-version"); if (configVersionElement != null) { configVersionElement.setAttribute("minOccurs", "0"); } @@ -213,25 +209,25 @@ private static void makeConfigVersionOptional(Document doc) { *

* Post-processing the generated XSD is the standard workaround for this JAXB limitation. */ - private static void replaceSequenceWithAllForRootEntity(Document doc) { - Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + private static void replaceSequenceWithAllForRootEntity(final Document doc) { + final var complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); if (complexType == null) { System.err.println("Warning: Could not find hiveMQConfigEntity complexType"); return; } // Find the xs:sequence child - NodeList children = complexType.getChildNodes(); + final var children = complexType.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { - Node child = children.item(i); + final var child = children.item(i); if (child instanceof Element && "sequence".equals(child.getLocalName())) { // Create new xs:all element - Element allElement = doc.createElementNS(XS_NAMESPACE, "xs:all"); + final var allElement = doc.createElementNS(XS_NAMESPACE, "xs:all"); // Move all children from sequence to all - NodeList sequenceChildren = child.getChildNodes(); + final var sequenceChildren = child.getChildNodes(); while (sequenceChildren.getLength() > 0) { - Node seqChild = sequenceChildren.item(0); + final var seqChild = sequenceChildren.item(0); allElement.appendChild(seqChild); } @@ -258,7 +254,7 @@ private static void replaceSequenceWithAllForRootEntity(Document doc) { * only at the element level, not properly handling the inheritance hierarchy with * {@code @XmlElementWrapper}. */ - private static void fixMqttListeners(Document doc) { + private static void fixMqttListeners(final Document doc) { fixListenerElement(doc, "mqtt-listeners", new String[]{ "tcp-listener", "tls-tcp-listener", "websocket-listener", "tls-websocket-listener" }); @@ -269,7 +265,7 @@ private static void fixMqttListeners(Document doc) { *

* See {@link #fixMqttListeners(Document)} for explanation of why this post-processing is needed. */ - private static void fixMqttSnListeners(Document doc) { + private static void fixMqttSnListeners(final Document doc) { fixListenerElement(doc, "mqtt-sn-listeners", new String[]{ "udp-listener", "udp-broadcast-listener" }); @@ -282,16 +278,16 @@ private static void fixMqttSnListeners(Document doc) { * @param elementName the wrapper element name (e.g., "mqtt-listeners") * @param listenerTypes the concrete listener element names to include in the choice */ - private static void fixListenerElement(Document doc, String elementName, String[] listenerTypes) { + private static void fixListenerElement(final Document doc, final String elementName, final String[] listenerTypes) { // Find the element in hiveMQConfigEntity - Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + final var complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); if (complexType == null) return; - Element listenersElement = findChildElementByName(complexType, elementName); + final var listenersElement = findChildElementByName(complexType, elementName); if (listenersElement == null) return; // Find the inner complexType - Element innerComplexType = findFirstChildElement(listenersElement, "complexType"); + final var innerComplexType = findFirstChildElement(listenersElement, "complexType"); if (innerComplexType == null) return; // Replace the content with xs:choice @@ -301,12 +297,12 @@ private static void fixListenerElement(Document doc, String elementName, String[ } // Create xs:choice with all listener types - Element choice = doc.createElementNS(XS_NAMESPACE, "xs:choice"); + final var choice = doc.createElementNS(XS_NAMESPACE, "xs:choice"); choice.setAttribute("minOccurs", "0"); choice.setAttribute("maxOccurs", "unbounded"); - for (String listenerType : listenerTypes) { - Element elementRef = doc.createElementNS(XS_NAMESPACE, "xs:element"); + for (final var listenerType : listenerTypes) { + final var elementRef = doc.createElementNS(XS_NAMESPACE, "xs:element"); elementRef.setAttribute("ref", listenerType); choice.appendChild(elementRef); } @@ -336,14 +332,14 @@ private static void fixListenerElement(Document doc, String elementName, String[ * Note: Using {@code xs:any} alone avoids non-determinism issues that occur when mixing * named elements with {@code xs:any} in a choice. */ - private static void fixProtocolAdapters(Document doc) { - Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + private static void fixProtocolAdapters(final Document doc) { + final var complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); if (complexType == null) return; - Element adaptersElement = findChildElementByName(complexType, "protocol-adapters"); + final var adaptersElement = findChildElementByName(complexType, "protocol-adapters"); if (adaptersElement == null) return; - Element innerComplexType = findFirstChildElement(adaptersElement, "complexType"); + final var innerComplexType = findFirstChildElement(adaptersElement, "complexType"); if (innerComplexType == null) return; // Clear existing content @@ -353,14 +349,14 @@ private static void fixProtocolAdapters(Document doc) { // Create xs:sequence with xs:any (skip validation for all adapter elements) // This allows both and legacy adapter-specific elements like - Element sequence = doc.createElementNS(XS_NAMESPACE, "xs:sequence"); + final var sequence = doc.createElementNS(XS_NAMESPACE, "xs:sequence"); - Element choice = doc.createElementNS(XS_NAMESPACE, "xs:choice"); + final var choice = doc.createElementNS(XS_NAMESPACE, "xs:choice"); choice.setAttribute("minOccurs", "0"); choice.setAttribute("maxOccurs", "unbounded"); // Use xs:any alone to avoid non-determinism - Element anyElement = doc.createElementNS(XS_NAMESPACE, "xs:any"); + final var anyElement = doc.createElementNS(XS_NAMESPACE, "xs:any"); anyElement.setAttribute("processContents", "skip"); choice.appendChild(anyElement); @@ -383,21 +379,21 @@ private static void fixProtocolAdapters(Document doc) { * the field to be {@code List} or similar DOM types, which would change the Java API. * The adapter approach provides a cleaner Java API but requires schema post-processing. */ - private static void fixModules(Document doc) { - Element complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); + private static void fixModules(final Document doc) { + final var complexType = findComplexTypeByName(doc, "hiveMQConfigEntity"); if (complexType == null) return; - Element modulesElement = findChildElementByName(complexType, "modules"); + final var modulesElement = findChildElementByName(complexType, "modules"); if (modulesElement == null) return; // Remove the type attribute and add inline complexType with xs:any modulesElement.removeAttribute("type"); // Create inline complexType - Element innerComplexType = doc.createElementNS(XS_NAMESPACE, "xs:complexType"); - Element sequence = doc.createElementNS(XS_NAMESPACE, "xs:sequence"); + final var innerComplexType = doc.createElementNS(XS_NAMESPACE, "xs:complexType"); + final var sequence = doc.createElementNS(XS_NAMESPACE, "xs:sequence"); - Element anyElement = doc.createElementNS(XS_NAMESPACE, "xs:any"); + final var anyElement = doc.createElementNS(XS_NAMESPACE, "xs:any"); anyElement.setAttribute("processContents", "skip"); anyElement.setAttribute("minOccurs", "0"); anyElement.setAttribute("maxOccurs", "unbounded"); @@ -422,7 +418,7 @@ private static void fixModules(Document doc) { * {@code @XmlElementWrapper} does not support a {@code required} attribute - wrappers are * always generated as required in the schema even when the collection can be empty. */ - private static void fixDataCombinerEntity(Document doc) { + private static void fixDataCombinerEntity(final Document doc) { replaceSequenceWithAll(doc, "dataCombinerEntity"); replaceSequenceWithAll(doc, "dataCombiningEntity"); @@ -441,11 +437,11 @@ private static void fixDataCombinerEntity(Document doc) { * @param typeName the name of the complex type containing the element * @param elementName the name of the element to make optional */ - private static void makeElementOptionalInType(Document doc, String typeName, String elementName) { - Element complexType = findComplexTypeByName(doc, typeName); + private static void makeElementOptionalInType(final Document doc, final String typeName, final String elementName) { + final var complexType = findComplexTypeByName(doc, typeName); if (complexType == null) return; - Element element = findChildElementByName(complexType, elementName); + final var element = findChildElementByName(complexType, elementName); if (element != null) { element.setAttribute("minOccurs", "0"); } @@ -459,20 +455,20 @@ private static void makeElementOptionalInType(Document doc, String typeName, Str * @param doc the XSD document to modify * @param typeName the name of the complex type to modify */ - private static void replaceSequenceWithAll(Document doc, String typeName) { - Element complexType = findComplexTypeByName(doc, typeName); + private static void replaceSequenceWithAll(final Document doc, final String typeName) { + final var complexType = findComplexTypeByName(doc, typeName); if (complexType == null) return; - NodeList children = complexType.getChildNodes(); + final var children = complexType.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { - Node child = children.item(i); + final var child = children.item(i); if (child instanceof Element && "sequence".equals(child.getLocalName())) { - Element allElement = doc.createElementNS(XS_NAMESPACE, "xs:all"); + final var allElement = doc.createElementNS(XS_NAMESPACE, "xs:all"); // Move all children from sequence to all - NodeList sequenceChildren = child.getChildNodes(); + final var sequenceChildren = child.getChildNodes(); while (sequenceChildren.getLength() > 0) { - Node seqChild = sequenceChildren.item(0); + final var seqChild = sequenceChildren.item(0); allElement.appendChild(seqChild); } @@ -498,8 +494,8 @@ private static void replaceSequenceWithAll(Document doc, String typeName) { * (like {@code adminApiEntity} extending {@code enabledEntity}), the base type's elements * also need to be made optional, which requires modifying multiple generated complex types. */ - private static void fixEmptyElementTypes(Document doc) { - String[] typesToFix = { + private static void fixEmptyElementTypes(final Document doc) { + final String[] typesToFix = { "mqttSnConfigEntity", "adminApiEntity", "dynamicConfigEntity", @@ -508,7 +504,7 @@ private static void fixEmptyElementTypes(Document doc) { "enabledEntity" }; - for (String typeName : typesToFix) { + for (final var typeName : typesToFix) { makeAllChildrenOptional(doc, typeName); } } @@ -519,13 +515,13 @@ private static void fixEmptyElementTypes(Document doc) { * @param doc the XSD document to modify * @param typeName the name of the complex type whose children should be made optional */ - private static void makeAllChildrenOptional(Document doc, String typeName) { - Element complexType = findComplexTypeByName(doc, typeName); + private static void makeAllChildrenOptional(final Document doc, final String typeName) { + final var complexType = findComplexTypeByName(doc, typeName); if (complexType == null) return; - NodeList elements = complexType.getElementsByTagNameNS(XS_NAMESPACE, "element"); + final var elements = complexType.getElementsByTagNameNS(XS_NAMESPACE, "element"); for (int i = 0; i < elements.getLength(); i++) { - Element element = (Element) elements.item(i); + final var element = (Element) elements.item(i); if (!element.hasAttribute("minOccurs")) { element.setAttribute("minOccurs", "0"); } @@ -547,8 +543,8 @@ private static void makeAllChildrenOptional(Document doc, String typeName) { * Bean Validation annotations (like {@code @Min}, {@code @Max}, {@code @Pattern}) are also * not translated to XSD constraints by JAXB. */ - private static void addCustomSimpleTypes(Document doc) { - Element schemaElement = doc.getDocumentElement(); + private static void addCustomSimpleTypes(final Document doc) { + final var schemaElement = doc.getDocumentElement(); addSimpleTypeIfNotExists(doc, schemaElement, "port", "xs:int", new String[]{"minInclusive", "0"}, new String[]{"maxInclusive", "65535"}); @@ -576,10 +572,10 @@ private static void addCustomSimpleTypes(Document doc) { new String[]{"minInclusive", "0"}); } - private static boolean simpleTypeExists(Document doc, String name) { - NodeList simpleTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "simpleType"); + private static boolean simpleTypeExists(final Document doc, final String name) { + final var simpleTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "simpleType"); for (int i = 0; i < simpleTypes.getLength(); i++) { - Element st = (Element) simpleTypes.item(i); + final var st = (Element) simpleTypes.item(i); if (name.equals(st.getAttribute("name"))) { return true; } @@ -587,29 +583,29 @@ private static boolean simpleTypeExists(Document doc, String name) { return false; } - private static void addSimpleTypeIfNotExists(Document doc, Element parent, String name, String baseType, String[]... facets) { + private static void addSimpleTypeIfNotExists(final Document doc, final Element parent, final String name, final String baseType, final String[]... facets) { if (simpleTypeExists(doc, name)) { return; } addSimpleType(doc, parent, name, baseType, facets); } - private static void addSimpleTypeWithEnumerationIfNotExists(Document doc, Element parent, String name, String baseType, String... values) { + private static void addSimpleTypeWithEnumerationIfNotExists(final Document doc, final Element parent, final String name, final String baseType, final String... values) { if (simpleTypeExists(doc, name)) { return; } addSimpleTypeWithEnumeration(doc, parent, name, baseType, values); } - private static void addSimpleType(Document doc, Element parent, String name, String baseType, String[]... facets) { - Element simpleType = doc.createElementNS(XS_NAMESPACE, "xs:simpleType"); + private static void addSimpleType(final Document doc, final Element parent, final String name, final String baseType, final String[]... facets) { + final var simpleType = doc.createElementNS(XS_NAMESPACE, "xs:simpleType"); simpleType.setAttribute("name", name); - Element restriction = doc.createElementNS(XS_NAMESPACE, "xs:restriction"); + final var restriction = doc.createElementNS(XS_NAMESPACE, "xs:restriction"); restriction.setAttribute("base", baseType); - for (String[] facet : facets) { - Element facetElement = doc.createElementNS(XS_NAMESPACE, "xs:" + facet[0]); + for (final String[] facet : facets) { + final var facetElement = doc.createElementNS(XS_NAMESPACE, "xs:" + facet[0]); facetElement.setAttribute("value", facet[1]); restriction.appendChild(facetElement); } @@ -618,15 +614,15 @@ private static void addSimpleType(Document doc, Element parent, String name, Str parent.appendChild(simpleType); } - private static void addSimpleTypeWithEnumeration(Document doc, Element parent, String name, String baseType, String... values) { - Element simpleType = doc.createElementNS(XS_NAMESPACE, "xs:simpleType"); + private static void addSimpleTypeWithEnumeration(final Document doc, final Element parent, final String name, final String baseType, final String... values) { + final var simpleType = doc.createElementNS(XS_NAMESPACE, "xs:simpleType"); simpleType.setAttribute("name", name); - Element restriction = doc.createElementNS(XS_NAMESPACE, "xs:restriction"); + final var restriction = doc.createElementNS(XS_NAMESPACE, "xs:restriction"); restriction.setAttribute("base", baseType); - for (String value : values) { - Element enumElement = doc.createElementNS(XS_NAMESPACE, "xs:enumeration"); + for (final String value : values) { + final var enumElement = doc.createElementNS(XS_NAMESPACE, "xs:enumeration"); enumElement.setAttribute("value", value); restriction.appendChild(enumElement); } @@ -637,10 +633,10 @@ private static void addSimpleTypeWithEnumeration(Document doc, Element parent, S // Helper methods for DOM traversal - private static Element findComplexTypeByName(Document doc, String name) { - NodeList complexTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "complexType"); + private static Element findComplexTypeByName(final Document doc, final String name) { + final var complexTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "complexType"); for (int i = 0; i < complexTypes.getLength(); i++) { - Element ct = (Element) complexTypes.item(i); + final var ct = (Element) complexTypes.item(i); if (name.equals(ct.getAttribute("name"))) { return ct; } @@ -648,17 +644,16 @@ private static Element findComplexTypeByName(Document doc, String name) { return null; } - private static Element findChildElementByName(Element parent, String elementName) { + private static Element findChildElementByName(final Element parent, final String elementName) { // Search through xs:all or xs:sequence children - NodeList children = parent.getChildNodes(); + final var children = parent.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { - Node child = children.item(i); - if (child instanceof Element) { - Element childElement = (Element) child; - String localName = childElement.getLocalName(); + final var child = children.item(i); + if (child instanceof final Element childElement) { + final var localName = childElement.getLocalName(); if ("all".equals(localName) || "sequence".equals(localName)) { // Search within all/sequence - Element found = findElementWithNameAttribute(childElement, elementName); + final var found = findElementWithNameAttribute(childElement, elementName); if (found != null) return found; } } @@ -666,16 +661,15 @@ private static Element findChildElementByName(Element parent, String elementName return null; } - private static Element findElementWithNameAttribute(Element parent, String nameValue) { - NodeList children = parent.getChildNodes(); + private static Element findElementWithNameAttribute(final Element parent, final String nameValue) { + final var children = parent.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { - Node child = children.item(i); - if (child instanceof Element) { - Element childElement = (Element) child; + final var child = children.item(i); + if (child instanceof final Element childElement) { if ("element".equals(childElement.getLocalName())) { // Check both "name" attribute and "ref" attribute - String name = childElement.getAttribute("name"); - String ref = childElement.getAttribute("ref"); + final var name = childElement.getAttribute("name"); + final var ref = childElement.getAttribute("ref"); if (nameValue.equals(name) || nameValue.equals(ref)) { return childElement; } @@ -685,10 +679,10 @@ private static Element findElementWithNameAttribute(Element parent, String nameV return null; } - private static Element findFirstChildElement(Element parent, String localName) { - NodeList children = parent.getChildNodes(); + private static Element findFirstChildElement(final Element parent, final String localName) { + final var children = parent.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { - Node child = children.item(i); + final var child = children.item(i); if (child instanceof Element && localName.equals(child.getLocalName())) { return (Element) child; } From 4acf064188cd720aec1126451d04742e97fcc318 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Tue, 2 Dec 2025 10:19:00 +0100 Subject: [PATCH 07/10] Put config.xsd intor resources for version tracking --- hivemq-edge/build.gradle.kts | 16 + hivemq-edge/src/main/resources/config.xsd | 2093 +++++++++++++++++++++ 2 files changed, 2109 insertions(+) create mode 100644 hivemq-edge/src/main/resources/config.xsd diff --git a/hivemq-edge/build.gradle.kts b/hivemq-edge/build.gradle.kts index 70b3f0694c..06adec5de4 100644 --- a/hivemq-edge/build.gradle.kts +++ b/hivemq-edge/build.gradle.kts @@ -633,3 +633,19 @@ tasks.jar { dependsOn(generateXsd) from(generateXsd.map { it.outputs.files }) } + +// Copy XSD to resources directory for version control +val copyXsdToResources by tasks.registering(Copy::class) { + group = "build" + description = "Copies generated XSD to src/main/resources for version control" + + dependsOn(generateXsd) + + from(generateXsd.map { it.outputs.files }) + into(file("src/main/resources")) +} + +// Run XSD copy as part of the build (after jar, avoiding circular dependency with processResources) +tasks.build { + dependsOn(copyXsdToResources) +} diff --git a/hivemq-edge/src/main/resources/config.xsd b/hivemq-edge/src/main/resources/config.xsd new file mode 100644 index 0000000000..9a2002b08a --- /dev/null +++ b/hivemq-edge/src/main/resources/config.xsd @@ -0,0 +1,2093 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From f787cd0d041a5ddbd497ad294e0ade5c0dd2c9e4 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Tue, 2 Dec 2025 12:56:31 +0100 Subject: [PATCH 08/10] Fixed incorrect handling of message-expiry and session-expiry --- hivemq-edge/build.gradle.kts | 7 +- .../mqtt/MessageExpiryConfigEntity.java | 73 ++++- .../mqtt/SessionExpiryConfigEntity.java | 73 ++++- hivemq-edge/src/main/resources/config.xsd | 260 +++++++++--------- .../hivemq/configuration/GenSchemaMain.java | 130 +++++++++ 5 files changed, 403 insertions(+), 140 deletions(-) diff --git a/hivemq-edge/build.gradle.kts b/hivemq-edge/build.gradle.kts index 06adec5de4..98cbb7959f 100644 --- a/hivemq-edge/build.gradle.kts +++ b/hivemq-edge/build.gradle.kts @@ -628,13 +628,8 @@ val generateXsd by tasks.registering(JavaExec::class) { } } -// Include the generated XSD in the jar (runs after test compilation) -tasks.jar { - dependsOn(generateXsd) - from(generateXsd.map { it.outputs.files }) -} - // Copy XSD to resources directory for version control +// The XSD is included in the jar automatically via processResources from src/main/resources val copyXsdToResources by tasks.registering(Copy::class) { group = "build" description = "Copies generated XSD to src/main/resources for version control" diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/MessageExpiryConfigEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/MessageExpiryConfigEntity.java index 9c79570f3f..db2b2033ac 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/MessageExpiryConfigEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/MessageExpiryConfigEntity.java @@ -15,16 +15,30 @@ */ package com.hivemq.configuration.entity.mqtt; +import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAnyElement; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlMixed; import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import static com.hivemq.configuration.entity.mqtt.MqttConfigurationDefaults.MAX_EXPIRY_INTERVAL_DEFAULT; /** + * Configuration entity for message expiry settings. + *

+ * Supports two XML formats for backwards compatibility: + *

    + *
  • Simple format: {@code 123}
  • + *
  • Nested format: {@code 123}
  • + *
+ * Always writes in nested format for consistency. + * * @author Florian Limpöck * @since 4.0.0 */ @@ -33,12 +47,65 @@ @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class MessageExpiryConfigEntity { + // For reading: captures both text content and child elements + @XmlMixed + @XmlAnyElement + private List content = new ArrayList<>(); + + // For writing: outputs as value @XmlElement(name = "max-interval", defaultValue = "4294967296") - // => 136 Years = Unsigned Integer Max Value in seconds - private long maxInterval = MAX_EXPIRY_INTERVAL_DEFAULT; + private Long maxIntervalForWrite; + + // Cached parsed value + private Long parsedMaxInterval; public long getMaxInterval() { - return maxInterval; + if (parsedMaxInterval == null) { + parsedMaxInterval = parseValue(); + } + return parsedMaxInterval != null ? parsedMaxInterval : MAX_EXPIRY_INTERVAL_DEFAULT; + } + + /** + * Called by JAXB before marshalling to ensure the write field is populated. + */ + @SuppressWarnings("unused") + void beforeMarshal(final Marshaller marshaller) { + maxIntervalForWrite = getMaxInterval(); + content = null; // Clear mixed content for clean output + } + + /** + * Parses the value from either the @XmlElement field (nested format) or the mixed content (simple format). + *

+ * Priority: + * 1. If maxIntervalForWrite was set by JAXB via @XmlElement, use it (nested format) + * 2. Otherwise, check the mixed content for simple text format + */ + private Long parseValue() { + // First check if JAXB parsed the nested element + if (maxIntervalForWrite != null) { + return maxIntervalForWrite; + } + + // Otherwise, check mixed content for simple text format + if (content == null) { + return null; + } + for (final Object item : content) { + if (item instanceof String text) { + // Simple text format: 123 + final String trimmed = text.trim(); + if (!trimmed.isEmpty()) { + try { + return Long.parseLong(trimmed); + } catch (final NumberFormatException e) { + // Whitespace or invalid text, ignore + } + } + } + } + return null; } @Override diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/SessionExpiryConfigEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/SessionExpiryConfigEntity.java index 05ca5e49d8..aa8f6cea5f 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/SessionExpiryConfigEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/mqtt/SessionExpiryConfigEntity.java @@ -15,16 +15,30 @@ */ package com.hivemq.configuration.entity.mqtt; +import jakarta.xml.bind.Marshaller; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAnyElement; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlMixed; import jakarta.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import static com.hivemq.mqtt.message.connect.Mqtt5CONNECT.SESSION_EXPIRY_MAX; /** + * Configuration entity for session expiry settings. + *

+ * Supports two XML formats for backwards compatibility: + *

    + *
  • Simple format: {@code 123}
  • + *
  • Nested format: {@code 123}
  • + *
+ * Always writes in nested format for consistency. + * * @author Florian Limpöck * @since 4.0.0 */ @@ -33,12 +47,65 @@ @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class SessionExpiryConfigEntity { + // For reading: captures both text content and child elements + @XmlMixed + @XmlAnyElement + private List content = new ArrayList<>(); + + // For writing: outputs as value @XmlElement(name = "max-interval", defaultValue = "4294967295") - // => 136 Years = Unsigned Integer Max Value in seconds - private long maxInterval = SESSION_EXPIRY_MAX; + private Long maxIntervalForWrite; + + // Cached parsed value + private Long parsedMaxInterval; public long getMaxInterval() { - return maxInterval; + if (parsedMaxInterval == null) { + parsedMaxInterval = parseValue(); + } + return parsedMaxInterval != null ? parsedMaxInterval : SESSION_EXPIRY_MAX; + } + + /** + * Called by JAXB before marshalling to ensure the write field is populated. + */ + @SuppressWarnings("unused") + void beforeMarshal(final Marshaller marshaller) { + maxIntervalForWrite = getMaxInterval(); + content = null; // Clear mixed content for clean output + } + + /** + * Parses the value from either the @XmlElement field (nested format) or the mixed content (simple format). + *

+ * Priority: + * 1. If maxIntervalForWrite was set by JAXB via @XmlElement, use it (nested format) + * 2. Otherwise, check the mixed content for simple text format + */ + private Long parseValue() { + // First check if JAXB parsed the nested element + if (maxIntervalForWrite != null) { + return maxIntervalForWrite; + } + + // Otherwise, check mixed content for simple text format + if (content == null) { + return null; + } + for (final Object item : content) { + if (item instanceof String text) { + // Simple text format: 123 + final String trimmed = text.trim(); + if (!trimmed.isEmpty()) { + try { + return Long.parseLong(trimmed); + } catch (final NumberFormatException e) { + // Whitespace or invalid text, ignore + } + } + } + } + return null; } @Override diff --git a/hivemq-edge/src/main/resources/config.xsd b/hivemq-edge/src/main/resources/config.xsd index 9a2002b08a..bad3bc5e5b 100644 --- a/hivemq-edge/src/main/resources/config.xsd +++ b/hivemq-edge/src/main/resources/config.xsd @@ -180,7 +180,7 @@ - + @@ -202,7 +202,7 @@ - + @@ -214,7 +214,7 @@ - + @@ -228,7 +228,7 @@ - + @@ -243,7 +243,7 @@ - + @@ -254,7 +254,7 @@ - + @@ -264,7 +264,7 @@ - + @@ -276,7 +276,7 @@ - + @@ -292,7 +292,7 @@ - + @@ -326,17 +326,17 @@ - + - + - + @@ -350,7 +350,7 @@ - + @@ -360,7 +360,7 @@ - + @@ -376,15 +376,15 @@ - + - + - + @@ -402,27 +402,31 @@ - + - + + + - + - + + + - + @@ -436,7 +440,7 @@ - + @@ -452,43 +456,43 @@ - + - + - + - + - + - + - + - + @@ -518,7 +522,7 @@ - + @@ -530,7 +534,7 @@ - + @@ -544,7 +548,7 @@ - + @@ -554,7 +558,7 @@ - + @@ -564,7 +568,7 @@ - + @@ -586,7 +590,7 @@ - + @@ -600,7 +604,7 @@ - + @@ -614,7 +618,7 @@ - + @@ -628,7 +632,7 @@ - + @@ -642,7 +646,7 @@ - + @@ -660,7 +664,7 @@ - + @@ -676,7 +680,7 @@ - + @@ -690,7 +694,7 @@ - + @@ -704,7 +708,7 @@ - + @@ -718,7 +722,7 @@ - + @@ -732,7 +736,7 @@ - + @@ -742,7 +746,7 @@ - + @@ -752,7 +756,7 @@ - + @@ -766,7 +770,7 @@ - + @@ -786,7 +790,7 @@ - + @@ -806,7 +810,7 @@ - + @@ -822,7 +826,7 @@ - + @@ -832,7 +836,7 @@ - + @@ -844,7 +848,7 @@ - + @@ -858,7 +862,7 @@ - + @@ -870,7 +874,7 @@ - + @@ -884,7 +888,7 @@ - + @@ -904,7 +908,7 @@ - + @@ -918,7 +922,7 @@ - + @@ -930,13 +934,13 @@ - + - + @@ -956,7 +960,7 @@ - + @@ -972,7 +976,7 @@ - + @@ -984,13 +988,13 @@ - + - + @@ -1010,7 +1014,7 @@ - + @@ -1024,7 +1028,7 @@ - + @@ -1042,7 +1046,7 @@ - + @@ -1054,7 +1058,7 @@ - + @@ -1064,7 +1068,7 @@ - + @@ -1086,7 +1090,7 @@ - + @@ -1112,7 +1116,7 @@ - + @@ -1126,7 +1130,7 @@ - + @@ -1138,7 +1142,7 @@ - + @@ -1156,7 +1160,7 @@ - + @@ -1164,7 +1168,7 @@ - + @@ -1178,7 +1182,7 @@ - + @@ -1194,7 +1198,7 @@ - + @@ -1212,7 +1216,7 @@ - + @@ -1222,7 +1226,7 @@ - + @@ -1238,13 +1242,13 @@ - + - + @@ -1282,7 +1286,7 @@ - + @@ -1294,7 +1298,7 @@ - + @@ -1308,7 +1312,7 @@ - + @@ -1320,7 +1324,7 @@ - + @@ -1336,7 +1340,7 @@ - + @@ -1346,7 +1350,7 @@ - + @@ -1374,7 +1378,7 @@ - + @@ -1386,7 +1390,7 @@ - + @@ -1396,7 +1400,7 @@ - + @@ -1410,7 +1414,7 @@ - + @@ -1424,7 +1428,7 @@ - + @@ -1438,7 +1442,7 @@ - + @@ -1454,7 +1458,7 @@ - + @@ -1464,7 +1468,7 @@ - + @@ -1478,7 +1482,7 @@ - + @@ -1496,7 +1500,7 @@ - + @@ -1514,7 +1518,7 @@ - + @@ -1526,7 +1530,7 @@ - + @@ -1542,13 +1546,13 @@ - + - + @@ -1564,7 +1568,7 @@ - + @@ -1578,7 +1582,7 @@ - + @@ -1602,7 +1606,7 @@ - + @@ -1616,7 +1620,7 @@ - + @@ -1632,7 +1636,7 @@ - + @@ -1656,7 +1660,7 @@ - + @@ -1672,7 +1676,7 @@ - + @@ -1680,7 +1684,7 @@ - + @@ -1694,7 +1698,7 @@ - + @@ -1710,7 +1714,7 @@ - + @@ -1724,7 +1728,7 @@ - + @@ -1734,7 +1738,7 @@ - + @@ -1744,7 +1748,7 @@ - + @@ -1764,7 +1768,7 @@ - + @@ -1774,7 +1778,7 @@ - + @@ -1788,7 +1792,7 @@ - + @@ -1806,7 +1810,7 @@ - + @@ -1820,7 +1824,7 @@ - + @@ -1834,7 +1838,7 @@ - + @@ -1852,7 +1856,7 @@ - + @@ -1864,7 +1868,7 @@ - + @@ -1886,7 +1890,7 @@ - + @@ -1898,7 +1902,7 @@ - + @@ -1922,7 +1926,7 @@ - + @@ -1936,7 +1940,7 @@ - + diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index 49ded3b4be..4dca23b2f4 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -160,6 +160,9 @@ private static void postProcessSchema(final Document doc) { fixModules(doc); fixDataCombinerEntity(doc); fixEmptyElementTypes(doc); + fixExpiryConfigTypes(doc); + fixMqttConfigTypes(doc); + addMixedContentToSequenceTypes(doc); addCustomSimpleTypes(doc); } @@ -528,6 +531,133 @@ private static void makeAllChildrenOptional(final Document doc, final String typ } } + /** + * Fixes expiry config types to make the max-interval element optional. + *

+ * Why this is needed: + * The session-expiry and message-expiry elements support both simple text format + * ({@code 123}) and nested element format + * ({@code 123}). + * The max-interval element must be optional to support the simple text format. + *

+ * Why JAXB cannot express this directly: + * The entity uses {@code @XmlMixed} + {@code @XmlAnyElement} for flexible reading + * and {@code @XmlElement} for structured writing. JAXB generates the max-interval + * as required by default. + */ + private static void fixExpiryConfigTypes(final Document doc) { + makeElementOptionalInType(doc, "sessionExpiryConfigEntity", "max-interval"); + makeElementOptionalInType(doc, "messageExpiryConfigEntity", "max-interval"); + } + + /** + * Fixes MQTT configuration types to use xs:all instead of xs:sequence. + *

+ * Why this is needed: + * The old hand-written XSD used {@code xs:all} for MQTT config types, allowing elements + * in any order. Existing config files rely on this flexibility. The generated XSD uses + * {@code xs:sequence} which requires strict element ordering. + *

+ * Why JAXB cannot express this directly: + * See {@link #replaceSequenceWithAllForRootEntity(Document)} for the explanation. + */ + private static void fixMqttConfigTypes(final Document doc) { + // MQTT configuration types that need flexible element ordering + final String[] typesToFix = { + "keepAliveConfigEntity", + "packetsConfigEntity", + "qoSConfigEntity", + "queuedMessagesConfigEntity", + "receiveMaximumConfigEntity", + "retainedMessagesConfigEntity", + "sharedSubscriptionsConfigEntity", + "subscriptionIdentifierConfigEntity", + "topicAliasConfigEntity", + "wildcardSubscriptionsConfigEntity" + }; + + for (final var typeName : typesToFix) { + replaceSequenceWithAll(doc, typeName); + } + } + + /** + * Adds mixed="true" attribute to complex types that use xs:sequence to allow whitespace + * between elements when JAXB marshals with pretty-printing. + *

+ * Why this is needed: + * By default, XSD complex types with {@code xs:sequence} have "element-only" content model, + * which means text nodes (including whitespace) are not allowed between child elements. + * When JAXB marshals XML with formatting enabled, it adds newlines and indentation as text + * nodes, which violates the schema constraint. + *

+ * Why JAXB cannot express this directly: + * There is no JAXB annotation to generate {@code mixed="true"} on complex types. + * JAXB assumes element-only content for most types. The {@code @XmlMixed} annotation exists + * but only works with {@code @XmlAnyElement} for capturing arbitrary mixed content, not for + * simply allowing whitespace in formatted output. + *

+ * Adding {@code mixed="true"} tells the XSD validator to allow text content (whitespace) + * between child elements, making the schema compatible with pretty-printed XML output. + *

+ * Important: When a type extends another type via {@code xs:complexContent/xs:extension}, + * both the base and derived types must have the same mixed content model. This method handles + * both direct sequence types and types that extend other types. + */ + private static void addMixedContentToSequenceTypes(final Document doc) { + final var complexTypes = doc.getElementsByTagNameNS(XS_NAMESPACE, "complexType"); + for (int i = 0; i < complexTypes.getLength(); i++) { + final var complexType = (Element) complexTypes.item(i); + + // Check if this complex type has a direct xs:sequence child + if (hasDirectSequenceChild(complexType)) { + complexType.setAttribute("mixed", "true"); + continue; + } + + // Check if this complex type extends another type (xs:complexContent/xs:extension) + // If so, it must also be mixed to match the base type + if (hasComplexContentExtension(complexType)) { + complexType.setAttribute("mixed", "true"); + } + } + } + + /** + * Checks if a complex type has a direct xs:sequence child element. + */ + private static boolean hasDirectSequenceChild(final Element complexType) { + final var children = complexType.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + final var child = children.item(i); + if (child instanceof Element && "sequence".equals(child.getLocalName())) { + return true; + } + } + return false; + } + + /** + * Checks if a complex type uses xs:complexContent with xs:extension (type inheritance). + */ + private static boolean hasComplexContentExtension(final Element complexType) { + final var children = complexType.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + final var child = children.item(i); + if (child instanceof Element && "complexContent".equals(child.getLocalName())) { + // Check for xs:extension child + final var complexContentChildren = child.getChildNodes(); + for (int j = 0; j < complexContentChildren.getLength(); j++) { + final var ccChild = complexContentChildren.item(j); + if (ccChild instanceof Element && "extension".equals(ccChild.getLocalName())) { + return true; + } + } + } + } + return false; + } + /** * Adds custom simple types for value constraints (only if they don't already exist). *

From 6c4990027bf74f03ff46c0a2c3c416b5071fff89 Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Tue, 2 Dec 2025 17:28:17 +0100 Subject: [PATCH 09/10] Handling optional fields --- hivemq-edge/src/main/resources/config.xsd | 174 ++++++++---- .../hivemq/configuration/GenSchemaMain.java | 252 +++++++++++++++++- .../reader/PulseExtractorTest.java | 7 +- 3 files changed, 364 insertions(+), 69 deletions(-) diff --git a/hivemq-edge/src/main/resources/config.xsd b/hivemq-edge/src/main/resources/config.xsd index bad3bc5e5b..815244111c 100644 --- a/hivemq-edge/src/main/resources/config.xsd +++ b/hivemq-edge/src/main/resources/config.xsd @@ -136,7 +136,7 @@ - + @@ -284,7 +284,7 @@ - + @@ -324,11 +324,11 @@ - + - + @@ -374,7 +374,7 @@ - + @@ -392,7 +392,7 @@ - + @@ -408,7 +408,7 @@ - + @@ -420,7 +420,7 @@ - + @@ -454,11 +454,11 @@ - + - + @@ -466,21 +466,21 @@ - + - + - + - + @@ -648,25 +648,25 @@ - + - + - + - + - + - + - + - + @@ -676,7 +676,7 @@ - + @@ -748,11 +748,11 @@ - + - + - + @@ -782,47 +782,47 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -936,7 +936,7 @@ - + @@ -952,9 +952,9 @@ - + - + @@ -972,7 +972,7 @@ - + @@ -990,7 +990,7 @@ - + @@ -1006,9 +1006,9 @@ - + - + @@ -1042,19 +1042,19 @@ - + - + - + @@ -1136,7 +1136,7 @@ - + @@ -1594,7 +1594,7 @@ - + @@ -1648,7 +1648,7 @@ - + @@ -1750,15 +1750,15 @@ - + - + - + @@ -1772,7 +1772,7 @@ - + @@ -2094,4 +2094,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index 4dca23b2f4..bcb5307eb6 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -573,12 +573,43 @@ private static void fixMqttConfigTypes(final Document doc) { "sharedSubscriptionsConfigEntity", "subscriptionIdentifierConfigEntity", "topicAliasConfigEntity", - "wildcardSubscriptionsConfigEntity" + "wildcardSubscriptionsConfigEntity", + // Restriction types + "restrictionsEntity", + // Bridge types that need flexible element ordering + "mqttBridgeEntity", + "remoteBrokerEntity", + "remoteSubscriptionEntity", + "localSubscriptionEntity", + "loopPreventionEntity", + "bridgeMqttEntity", + "forwardedTopicEntity", + // Security config types + "securityConfigEntity", + // Pulse config types - note: these use hyphenated names in XSD + "managed-asset" }; for (final var typeName : typesToFix) { replaceSequenceWithAll(doc, typeName); } + + // Make bridge elements optional that have defaults + makeAllChildrenOptional(doc, "mqttBridgeEntity"); + makeAllChildrenOptional(doc, "remoteBrokerEntity"); + makeAllChildrenOptional(doc, "remoteSubscriptionEntity"); + makeAllChildrenOptional(doc, "localSubscriptionEntity"); + makeAllChildrenOptional(doc, "bridgeMqttEntity"); + makeAllChildrenOptional(doc, "forwardedTopicEntity"); + + // Fix element references in remoteBrokerEntity to use proper types instead of global refs + // JAXB generates ref="mqtt" which references the global mqtt element with xs:anyType + // We need to replace it with an inline element of type bridgeMqttEntity + replaceElementRefWithTypedElement(doc, "remoteBrokerEntity", "mqtt", "bridgeMqttEntity"); + + // API listener entity: bind-address is optional (has default "0.0.0.0" in Java) + // The Java entity has required=true but the old XSD allowed it to be optional + makeElementOptionalWithDefault(doc, "apiListenerEntity", "bind-address", "0.0.0.0"); } /** @@ -609,8 +640,8 @@ private static void addMixedContentToSequenceTypes(final Document doc) { for (int i = 0; i < complexTypes.getLength(); i++) { final var complexType = (Element) complexTypes.item(i); - // Check if this complex type has a direct xs:sequence child - if (hasDirectSequenceChild(complexType)) { + // Check if this complex type has a direct xs:sequence or xs:all child + if (hasDirectSequenceOrAllChild(complexType)) { complexType.setAttribute("mixed", "true"); continue; } @@ -624,14 +655,18 @@ private static void addMixedContentToSequenceTypes(final Document doc) { } /** - * Checks if a complex type has a direct xs:sequence child element. + * Checks if a complex type has a direct xs:sequence or xs:all child element. + * Both need mixed="true" to allow whitespace (formatting) between child elements. */ - private static boolean hasDirectSequenceChild(final Element complexType) { + private static boolean hasDirectSequenceOrAllChild(final Element complexType) { final var children = complexType.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { final var child = children.item(i); - if (child instanceof Element && "sequence".equals(child.getLocalName())) { - return true; + if (child instanceof Element) { + final String localName = child.getLocalName(); + if ("sequence".equals(localName) || "all".equals(localName)) { + return true; + } } } return false; @@ -700,6 +735,168 @@ private static void addCustomSimpleTypes(final Document doc) { addSimpleTypeIfNotExists(doc, schemaElement, "nonNegativeLong", "xs:long", new String[]{"minInclusive", "0"}); + + // MQTT configuration value constraints + // max-packet-size: 15 to 268435460 (MQTT spec) + addSimpleTypeIfNotExists(doc, schemaElement, "maxPacketSizeType", "xs:int", + new String[]{"minInclusive", "15"}, new String[]{"maxInclusive", "268435460"}); + + // server-receive-maximum and max-per-client: 1 to 65535 (MQTT spec) + addSimpleTypeIfNotExists(doc, schemaElement, "uint16NonZeroType", "xs:int", + new String[]{"minInclusive", "1"}, new String[]{"maxInclusive", "65535"}); + + // max-keep-alive: 1 to 65535 (MQTT spec) + addSimpleTypeIfNotExists(doc, schemaElement, "keepAliveType", "xs:int", + new String[]{"minInclusive", "1"}, new String[]{"maxInclusive", "65535"}); + + // session-expiry max-interval: 0 to 4294967295 (MQTT spec - uint32) + addSimpleTypeIfNotExists(doc, schemaElement, "sessionExpiryIntervalType", "xs:long", + new String[]{"minInclusive", "0"}, new String[]{"maxInclusive", "4294967295"}); + + // message-expiry max-interval: 0 to 4294967296 (allows disabling with max+1) + addSimpleTypeIfNotExists(doc, schemaElement, "messageExpiryIntervalType", "xs:long", + new String[]{"minInclusive", "0"}, new String[]{"maxInclusive", "4294967296"}); + + // max-queue-size: 1 or more + addSimpleTypeIfNotExists(doc, schemaElement, "maxQueueSizeType", "xs:long", + new String[]{"minInclusive", "1"}); + + // Restriction constraints + // max-connections: -1 (unlimited) or positive + addSimpleTypeIfNotExists(doc, schemaElement, "maxConnectionsType", "xs:long", + new String[]{"minInclusive", "-1"}); + + // max-client-id-length, max-topic-length: 1 to 65535 + addSimpleTypeIfNotExists(doc, schemaElement, "maxLengthType", "xs:int", + new String[]{"minInclusive", "1"}, new String[]{"maxInclusive", "65535"}); + + // no-connect-idle-timeout: 0 or more + addSimpleTypeIfNotExists(doc, schemaElement, "timeoutMsType", "xs:int", + new String[]{"minInclusive", "0"}); + + // incoming-bandwidth-throttling: 0 or more + addSimpleTypeIfNotExists(doc, schemaElement, "bandwidthType", "xs:long", + new String[]{"minInclusive", "0"}); + + // Apply the custom types to the relevant elements + applyValueConstraints(doc); + } + + /** + * Applies value constraint types to specific elements in the XSD. + *

+ * Why this is needed: + * JAXB generates elements with basic types (xs:int, xs:long) without value constraints. + * The old hand-written XSD had inline simpleType restrictions for many elements to enforce + * valid value ranges. This method replaces the basic types with custom constrained types. + */ + private static void applyValueConstraints(final Document doc) { + // packetsConfigEntity: max-packet-size + changeElementTypeInComplexType(doc, "packetsConfigEntity", "max-packet-size", "maxPacketSizeType"); + + // receiveMaximumConfigEntity: server-receive-maximum + changeElementTypeInComplexType(doc, "receiveMaximumConfigEntity", "server-receive-maximum", "uint16NonZeroType"); + + // topicAliasConfigEntity: max-per-client + changeElementTypeInComplexType(doc, "topicAliasConfigEntity", "max-per-client", "uint16NonZeroType"); + + // keepAliveConfigEntity: max-keep-alive + changeElementTypeInComplexType(doc, "keepAliveConfigEntity", "max-keep-alive", "keepAliveType"); + + // queuedMessagesConfigEntity: max-queue-size + changeElementTypeInComplexType(doc, "queuedMessagesConfigEntity", "max-queue-size", "maxQueueSizeType"); + + // sessionExpiryConfigEntity: max-interval + changeElementTypeInComplexType(doc, "sessionExpiryConfigEntity", "max-interval", "sessionExpiryIntervalType"); + + // messageExpiryConfigEntity: max-interval + changeElementTypeInComplexType(doc, "messageExpiryConfigEntity", "max-interval", "messageExpiryIntervalType"); + + // restrictionsEntity: various constraints + changeElementTypeInComplexType(doc, "restrictionsEntity", "max-connections", "maxConnectionsType"); + changeElementTypeInComplexType(doc, "restrictionsEntity", "max-client-id-length", "maxLengthType"); + changeElementTypeInComplexType(doc, "restrictionsEntity", "max-topic-length", "maxLengthType"); + changeElementTypeInComplexType(doc, "restrictionsEntity", "no-connect-idle-timeout", "timeoutMsType"); + changeElementTypeInComplexType(doc, "restrictionsEntity", "incoming-bandwidth-throttling", "bandwidthType"); + + // listenerEntity: name must be non-empty (whitespace collapses to empty string) + changeElementTypeInComplexType(doc, "listenerEntity", "name", "nonEmptyString"); + + // Pulse managed-asset: id attribute must be UUID + changeAttributeTypeInComplexType(doc, "managed-asset", "id", "uuidType"); + + // Pulse mapping: id attribute must be UUID + changeAttributeTypeInComplexType(doc, "mapping", "id", "uuidType"); + } + + /** + * Changes the type attribute of a specific element within a complex type. + */ + private static void changeElementTypeInComplexType( + final Document doc, + final String complexTypeName, + final String elementName, + final String newType) { + final var complexType = findComplexTypeByName(doc, complexTypeName); + if (complexType == null) { + return; + } + + final var elements = complexType.getElementsByTagNameNS(XS_NAMESPACE, "element"); + for (int i = 0; i < elements.getLength(); i++) { + final var element = (Element) elements.item(i); + if (elementName.equals(element.getAttribute("name"))) { + element.setAttribute("type", newType); + break; + } + } + } + + /** + * Changes the type attribute of a specific attribute within a complex type. + */ + private static void changeAttributeTypeInComplexType( + final Document doc, + final String complexTypeName, + final String attributeName, + final String newType) { + final var complexType = findComplexTypeByName(doc, complexTypeName); + if (complexType == null) { + return; + } + + final var attributes = complexType.getElementsByTagNameNS(XS_NAMESPACE, "attribute"); + for (int i = 0; i < attributes.getLength(); i++) { + final var attr = (Element) attributes.item(i); + if (attributeName.equals(attr.getAttribute("name"))) { + attr.setAttribute("type", newType); + break; + } + } + } + + /** + * Makes a specific element optional (minOccurs="0") with a default value in a complex type. + */ + private static void makeElementOptionalWithDefault( + final Document doc, + final String complexTypeName, + final String elementName, + final String defaultValue) { + final var complexType = findComplexTypeByName(doc, complexTypeName); + if (complexType == null) { + return; + } + + final var elements = complexType.getElementsByTagNameNS(XS_NAMESPACE, "element"); + for (int i = 0; i < elements.getLength(); i++) { + final var element = (Element) elements.item(i); + if (elementName.equals(element.getAttribute("name"))) { + element.setAttribute("minOccurs", "0"); + element.setAttribute("default", defaultValue); + break; + } + } } private static boolean simpleTypeExists(final Document doc, final String name) { @@ -819,4 +1016,45 @@ private static Element findFirstChildElement(final Element parent, final String } return null; } + + /** + * Replaces an element reference (ref="elementName") with an inline typed element in a complex type. + *

+ * Why this is needed: + * JAXB generates element references (ref="...") for {@code @XmlElementRef} annotated fields. + * This references the global element declaration which may have an inappropriate type + * (e.g., xs:anyType). For proper validation, we need inline elements with specific types. + * + * @param doc the XSD document to modify + * @param complexTypeName the name of the complex type containing the element reference + * @param elementName the element name to replace (the ref value) + * @param newTypeName the type to use for the new inline element + */ + private static void replaceElementRefWithTypedElement( + final Document doc, + final String complexTypeName, + final String elementName, + final String newTypeName) { + final var complexType = findComplexTypeByName(doc, complexTypeName); + if (complexType == null) return; + + // Find the element with ref="elementName" in the complex type + final var refElement = findChildElementByName(complexType, elementName); + if (refElement == null || !refElement.hasAttribute("ref")) { + return; + } + + // Preserve the minOccurs attribute if it exists + final String minOccurs = refElement.getAttribute("minOccurs"); + + // Remove ref attribute and set name and type instead + refElement.removeAttribute("ref"); + refElement.setAttribute("name", elementName); + refElement.setAttribute("type", newTypeName); + + // Restore minOccurs if it was present + if (!minOccurs.isEmpty()) { + refElement.setAttribute("minOccurs", minOccurs); + } + } } diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/PulseExtractorTest.java b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/PulseExtractorTest.java index a038681c52..a2bc43d71a 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/reader/PulseExtractorTest.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/reader/PulseExtractorTest.java @@ -347,8 +347,8 @@ public void whenSchemaIsMissing_thenApplyConfigFails() throws IOException { assertThatThrownBy(configFileReader::applyConfig).isInstanceOf(UnrecoverableException.class); assertThat(logCapture.isLogCaptured()).isTrue(); assertThat(logCapture.getLastCapturedLog().getLevel()).isEqualTo(Level.ERROR); - assertThat(logCapture.getLastCapturedLog().getFormattedMessage()).contains( - "Invalid content was found starting with element 'mapping'. One of '{schema}' is expected."); + // With xs:all content model, the error message format differs from xs:sequence + assertThat(logCapture.getLastCapturedLog().getFormattedMessage()).contains("One of '{schema}' is expected."); } @Test @@ -433,8 +433,9 @@ public void whenStatusIsInvalid_thenApplyConfigFails() throws IOException { assertThat(logCapture.getLastCapturedLog().getLevel()).isEqualTo(Level.ERROR); assertThat(logCapture.getLastCapturedLog().getFormattedMessage()).contains( "Value 'INVALID' is not facet-valid with respect to enumeration '[DRAFT, MISSING, REQUIRES_REMAPPING, STREAMING, UNMAPPED]'. It must be a value from the enumeration."); + // The type name is generated from the XSD, which uses a named type 'pulseAssetMappingStatus' assertThat(logCapture.getLastCapturedLog().getFormattedMessage()).contains( - "The value 'INVALID' of attribute 'status' on element 'mapping' is not valid with respect to its type, '#AnonType_statuspulseAssetMappingEntity'."); + "The value 'INVALID' of attribute 'status' on element 'mapping' is not valid with respect to its type, 'pulseAssetMappingStatus'."); } @ParameterizedTest From 6eba5f584cc8ac2942b3f9f9b390dc1ba277be3f Mon Sep 17 00:00:00 2001 From: Jochen Mader Date: Thu, 4 Dec 2025 12:00:27 +0100 Subject: [PATCH 10/10] Giving up --- .../hivemq/configuration/entity/api/AdminApiEntity.java | 2 +- .../java/com/hivemq/configuration/GenSchemaMain.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java index 2322969341..7933ed8401 100644 --- a/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java +++ b/hivemq-edge/src/main/java/com/hivemq/configuration/entity/api/AdminApiEntity.java @@ -39,7 +39,7 @@ public class AdminApiEntity extends EnabledEntity { @XmlElementRefs({ @XmlElementRef(required = false, type = HttpListenerEntity.class), @XmlElementRef(required = false, type = HttpsListenerEntity.class)}) - private @NotNull List listeners; + private @NotNull List listeners; @XmlElementRef(required = false) private @NotNull ApiJwsEntity jws; diff --git a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java index bcb5307eb6..7a0a531f13 100644 --- a/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java +++ b/hivemq-edge/src/test/java/com/hivemq/configuration/GenSchemaMain.java @@ -586,6 +586,10 @@ private static void fixMqttConfigTypes(final Document doc) { "forwardedTopicEntity", // Security config types "securityConfigEntity", + // LDAP authentication types - elements can appear in any order + "ldapAuthenticationEntity", + "ldapServerEntity", + "ldapSimpleBindEntity", // Pulse config types - note: these use hyphenated names in XSD "managed-asset" }; @@ -602,6 +606,11 @@ private static void fixMqttConfigTypes(final Document doc) { makeAllChildrenOptional(doc, "bridgeMqttEntity"); makeAllChildrenOptional(doc, "forwardedTopicEntity"); + // Make LDAP elements optional that have defaults + makeAllChildrenOptional(doc, "ldapAuthenticationEntity"); + makeAllChildrenOptional(doc, "ldapServerEntity"); + makeAllChildrenOptional(doc, "ldapSimpleBindEntity"); + // Fix element references in remoteBrokerEntity to use proper types instead of global refs // JAXB generates ref="mqtt" which references the global mqtt element with xs:anyType // We need to replace it with an inline element of type bridgeMqttEntity