From 3d5601797ff8157de0c8cf7a1a1b155d8b8c9d46 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 09:57:24 -0500 Subject: [PATCH 1/7] docs: add PRD for Saxon and JDOM2 dependency removal Add Product Requirements Document and implementation plan for removing Saxon-HE, xmlresolver, JDOM2, and jaxen dependencies from the schemagen module by replacing them with standard Java XML APIs. Key design decisions documented: - IndentingXMLStreamWriter with stack-based mixed content detection - Standard DOM/XPath APIs for XSD loading - Transformer-based DOM element serialization - TDD methodology with characterization tests first --- PRDs/20251231-saxon-jdom2-removal/PRD.md | 201 ++++++++ .../implementation-plan.md | 431 ++++++++++++++++++ 2 files changed, 632 insertions(+) create mode 100644 PRDs/20251231-saxon-jdom2-removal/PRD.md create mode 100644 PRDs/20251231-saxon-jdom2-removal/implementation-plan.md diff --git a/PRDs/20251231-saxon-jdom2-removal/PRD.md b/PRDs/20251231-saxon-jdom2-removal/PRD.md new file mode 100644 index 000000000..e10824fd6 --- /dev/null +++ b/PRDs/20251231-saxon-jdom2-removal/PRD.md @@ -0,0 +1,201 @@ +# PRD: Remove Saxon and JDOM2 Dependencies + +## Problem Statement + +The `schemagen` module depends on Saxon-HE (~5MB) and JDOM2 + jaxen (~500KB) for XML schema generation. These dependencies add significant size to the distribution and introduce external library dependencies where standard Java XML APIs would suffice. + +### Current State + +- **Saxon-HE** is used solely for an XSLT identity transform that adds indentation to generated XML schemas +- **xmlresolver** is a transitive dependency of Saxon (comment in pom.xml: "for saxon") +- **JDOM2** is used for: + - Parsing XSD resource files containing datatype definitions + - Executing XPath queries to extract schema elements + - Writing DOM elements to XMLStreamWriter via StAXStreamOutputter +- **jaxen** provides XPath support for JDOM2 + +### XSD Resource Files + +The following XSD files are loaded and processed via JDOM2 XPath: + +| Resource | Loaded By | XPath Query | +|----------|-----------|-------------| +| `/schema/xml/metaschema-datatypes.xsd` | `XmlCoreDatatypeProvider` | `/xs:schema/xs:simpleType` | +| `/schema/xml/metaschema-markup-line.xsd` | `XmlMarkupLineDatatypeProvider` | `/xs:schema/*` | +| `/schema/xml/metaschema-markup-multiline.xsd` | `XmlMarkupMultilineDatatypeProvider` | `/xs:schema/*` | +| `/schema/xml/metaschema-prose-base.xsd` | `XmlProseBaseDatatypeProvider` | `/xs:schema/xs:simpleType` | + +These files are packaged in the `core` module and accessed via `IModule.class.getResourceAsStream()`. + +### Why This Matters + +1. **Dependency bloat**: Saxon-HE alone is ~5MB, larger than many core modules +2. **Maintenance burden**: External dependencies require version updates and security monitoring +3. **Standard alternatives exist**: Java's built-in DOM, XPath, and Transformer APIs provide equivalent functionality +4. **Performance**: Current approach buffers entire schema for XSLT post-processing; streaming indentation is more efficient + +## Goals + +1. Remove Saxon-HE dependency from the project +2. Remove JDOM2 and jaxen dependencies from the project +3. Replace with standard Java XML APIs (no new dependencies) +4. Maintain identical schema generation output (except minor whitespace differences in documentation) +5. Improve performance through streaming indentation + +## Non-Goals + +- Changing the structure or content of generated schemas +- Modifying the public API of the schemagen module +- Adding new XML processing capabilities + +## Development Methodology + +### Test-Driven Development (MANDATORY) + +All implementation MUST follow strict TDD: + +1. **Write tests first** - Before any implementation code exists +2. **Watch tests fail** - Verify tests fail for the expected reason (not compilation errors) +3. **Write minimal code** - Implement just enough to make tests pass +4. **Refactor** - Clean up while keeping tests green + +### TDD Sequence for This PRD + +| Component | Test First | Then Implement | +|-----------|------------|----------------| +| `IndentingXMLStreamWriter` | Test indentation behavior with mock writer | Implement wrapper class | +| `XmlSchemaLoader` | Test XPath queries return expected elements | Implement DOM/XPath loader | +| `DomDatatypeContent` | Test DOM element serialization to XMLStreamWriter | Implement serialization | +| `XmlSchemaGenerator` changes | Verify existing tests pass with new approach | Remove Saxon, use IndentingXMLStreamWriter | + +### Test Requirements + +- **Characterization tests first**: Before replacing any existing code, write tests that capture current behavior +- **Verify tests pass**: With existing JDOM2/Saxon implementation +- **New classes**: 100% test coverage for public methods +- **Behavioral equivalence**: New implementations must pass the same tests as old implementations +- **Edge cases**: Empty documents, missing elements, malformed XML handling +- **Integration**: End-to-end schema generation tests must produce equivalent output + +### Text Production Testing (CRITICAL) + +The `IndentingXMLStreamWriter` must be tested against ALL XML text productions to ensure content is not corrupted: + +| Production | Test Requirement | +|------------|------------------| +| Element content | Proper indentation at each nesting level | +| Text content | NO added whitespace - text must be preserved exactly | +| Mixed content | Text + child elements must not have spurious whitespace | +| CDATA sections | Content must not be modified | +| Comments | Properly indented, content preserved | +| Processing instructions | Properly indented | +| Attributes | No indentation effect | +| XHTML in xs:documentation | Inline elements (``, ``) must not gain whitespace | + +**Why this matters**: The original Saxon XSLT used `suppress-indentation="xhtml:b xhtml:p"` specifically to prevent whitespace corruption in schema documentation. Our replacement must handle this correctly through mixed content detection. + +## Requirements + +### Functional Requirements + +1. **FR-1**: XML schemas generated after the change must be semantically equivalent to those generated before +2. **FR-2**: All existing schemagen tests must pass without modification (except whitespace assertions if any) +3. **FR-3**: XSD datatype resources must continue to be loaded and processed correctly +4. **FR-4**: Generated schemas must be properly indented for readability + +### Technical Requirements + +1. **TR-1**: Create `IndentingXMLStreamWriter` wrapper for streaming indentation +2. **TR-2**: Replace JDOM2 XML parsing with `javax.xml.parsers.DocumentBuilder` +3. **TR-3**: Replace JDOM2 XPath with `javax.xml.xpath.XPath` +4. **TR-4**: Replace JDOM2 element serialization with `javax.xml.transform.Transformer` +5. **TR-5**: Update `module-info.java` to remove Saxon.HE and org.jdom2 requirements +6. **TR-6**: Remove dependency declarations from pom.xml files + +## Success Metrics + +| Metric | Target | +|--------|--------| +| Dependencies removed | 4 (Saxon-HE, xmlresolver, jdom2, jaxen) | +| New dependencies added | 0 | +| Existing tests passing | 100% | +| JAR size reduction | ~5.5MB | +| Build verification | `mvn clean install -PCI -Prelease` passes | + +## Risks and Mitigations + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| Whitespace differences in output | High | Low | Document as expected; only affects formatting in xs:documentation | +| XPath behavior differences | Low | Medium | Comprehensive test coverage for XSD loading | +| Performance regression | Low | Low | Streaming approach should be faster than buffered XSLT | + +## Dependencies + +- No blocking dependencies on other work +- This change is isolated to the `schemagen` module + +## Design Decisions + +### Interface Compatibility + +The implementation will use the standard `XMLStreamWriter` interface rather than Woodstox's `XMLStreamWriter2` extension. Analysis shows that only standard `XMLStreamWriter` methods are used: + +- `writeStartDocument`, `writeEndDocument` +- `writeStartElement`, `writeEndElement` +- `writeDefaultNamespace`, `writeNamespace` +- `writeAttribute` +- `flush` + +This ensures compatibility with any StAX implementation. + +### Mixed Content Detection + +The `IndentingXMLStreamWriter` will use dynamic mixed content detection rather than element-specific suppression: + +1. Track `hasText` flag per element level using a stack +2. When `writeCharacters()` is called with non-whitespace text, set `hasText = true` +3. When `hasText` is true, suppress indentation for child elements +4. When closing an element, pop the stack to restore parent's state + +This approach: +- Is simpler than the Saxon XSLT's `suppress-indentation="xhtml:b xhtml:p"` approach +- Works correctly for any inline elements, not just a hardcoded list +- Automatically handles mixed content regardless of element names + +### Line Endings and Configurability + +| Setting | Value | Rationale | +|---------|-------|-----------| +| Line ending | `\n` (Unix) | Consistent across platforms; matches Saxon output | +| Indent size | 2 spaces | Fixed; matches existing output; no configurability needed | + +### Acceptable Whitespace Differences + +The following whitespace differences between Saxon XSLT and IndentingXMLStreamWriter output are acceptable: + +1. **xs:documentation content**: Saxon preserves original formatting; new approach adds structure indentation +2. **Empty element spacing**: Minor differences in element-only content are acceptable +3. **Trailing whitespace**: Any trailing whitespace differences are acceptable + +Semantic equivalence (XML parses to identical DOM) is required; formatting differences are acceptable. + +## Existing Test Coverage + +### Current Tests +- `XmlSuiteTest` - Integration tests for XML schema generation (uses JDOM2 for assertions) +- `JsonSuiteTest` - JSON schema generation (unaffected by this change) +- `MetaschemaModuleTest` - Module loading tests + +### Tests Requiring Update +- `XmlSuiteTest` uses JDOM2 (`StAXEventBuilder`, `XPathExpression`) for test assertions +- These must be converted to standard DOM/XPath APIs + +### New Tests Required +- `IndentingXMLStreamWriterTest` - Comprehensive text production tests +- `XmlSchemaLoaderTest` - Characterization tests for XPath queries +- `DomDatatypeContentTest` - DOM serialization tests + +## Related Documents + +- [Implementation Plan](./implementation-plan.md) diff --git a/PRDs/20251231-saxon-jdom2-removal/implementation-plan.md b/PRDs/20251231-saxon-jdom2-removal/implementation-plan.md new file mode 100644 index 000000000..8a11b9046 --- /dev/null +++ b/PRDs/20251231-saxon-jdom2-removal/implementation-plan.md @@ -0,0 +1,431 @@ +# Implementation Plan: Remove Saxon and JDOM2 Dependencies + +## Overview + +This plan removes Saxon-HE, xmlresolver, JDOM2, and jaxen dependencies from the schemagen module by replacing them with standard Java XML APIs. + +## PR Structure + +This work will be completed in a single PR since the changes are tightly coupled and cannot function independently. + +--- + +## TDD Requirement (MANDATORY) + +**All phases MUST follow Test-Driven Development.** + +### For New Components (IndentingXMLStreamWriter) + +1. **Write tests FIRST** - Before any implementation code +2. **Verify tests FAIL** - For the expected reason (not compilation errors) +3. **Write minimal implementation** - Just enough to pass +4. **Refactor** - Clean up while keeping tests green + +### For Replacing Existing Components (JDOM2 → Standard DOM) + +1. **Write characterization tests FIRST** - Tests that capture existing behavior of JDOM2 classes +2. **Verify tests PASS** - With current JDOM2 implementation +3. **Create new implementation** - Using standard DOM/XPath APIs +4. **Verify tests PASS** - With new implementation (behavioral equivalence) +5. **Delete old code** - Only after tests confirm equivalence + +**Enforcement:** No implementation code may be written until corresponding tests exist and verify the expected behavior. + +--- + +## PR 1: Remove Saxon and JDOM2 Dependencies + +**Branch**: `refactor/remove-saxon-jdom2` +**Target**: `develop` + +### Phase 1: Create IndentingXMLStreamWriter + +Create a streaming XML indentation wrapper to replace Saxon XSLT post-processing. + +#### Files Changed + +| File | Change Type | +|------|-------------| +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter.java` | Add | + +#### TDD Sequence + +1. **Write tests first** for `IndentingXMLStreamWriter` covering ALL text productions: + + **Element Structure Tests:** + - Test single element indentation + - Test nested element indentation (2+ levels) + - Test sibling elements at same level + - Test empty elements (``) + - Test elements with only whitespace content + + **Text Content Tests (CRITICAL - must not corrupt):** + - Test text content is NOT indented (no added whitespace inside text) + - Test mixed content (text + child elements) preserves text exactly + - Test inline elements within text (e.g., `

text bold more

`) + - Test whitespace-only text nodes are preserved + - Test text with leading/trailing whitespace is preserved + + **Special Content Tests:** + - Test CDATA sections are not indented internally + - Test comments are properly indented + - Test processing instructions are properly indented + - Test attributes (no indentation effect) + - Test namespace declarations + + **Schema Documentation Tests (xs:documentation with XHTML):** + - Test `` containing `` elements + - Test `` containing `` inline elements + - Test nested XHTML (paragraphs containing bold/italic) + - Verify no spurious whitespace added inside inline elements + +2. **Verify tests fail** - Class doesn't exist yet +3. **Implement** `IndentingXMLStreamWriter` +4. **Verify tests pass** + +#### Acceptance Criteria + +- [ ] Create `IndentingXMLStreamWriterTest` with comprehensive tests for all text productions +- [ ] Tests cover element indentation, text preservation, mixed content, CDATA, comments, PIs +- [ ] Tests specifically verify XHTML documentation content is not corrupted +- [ ] Verify tests fail (class not found) +- [ ] Create `IndentingXMLStreamWriter` class implementing `XMLStreamWriter` +- [ ] Wrapper delegates all calls to underlying writer +- [ ] Inserts newline + indentation before start elements (when not in mixed content) +- [ ] Inserts newline before end elements (at parent indent level, when not in mixed content) +- [ ] Tracks nesting depth for proper indentation +- [ ] Tracks "mixed content mode" to suppress indentation when text has been written +- [ ] Handles edge cases (empty elements, CDATA, comments, PIs) +- [ ] All tests pass + +--- + +### Phase 2: Replace JDOM2 with Standard DOM/XPath + +Replace JDOM2 XML parsing and XPath with javax.xml APIs. + +#### Files Changed + +| File | Change Type | +|------|-------------| +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java` | Add | +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContentTest.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContent.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2XmlSchemaLoader.java` | Delete | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2DatatypeContent.java` | Delete | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlDatatypeProvider.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlCoreDatatypeProvider.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlMarkupDatatypeProvider.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlProseBaseDatatypeProvider.java` | Modify | + +#### TDD Sequence (Characterization Tests First) + +**Step 1: Write characterization tests against existing JDOM2 implementation** + +1. Create `XmlSchemaLoaderTest` that tests `JDom2XmlSchemaLoader`: + - Test loading XSD from InputStream + - Test XPath query `/xs:schema/xs:simpleType` returns expected element count and names + - Test XPath query `/xs:schema/*` returns all child elements + - Test XPath query `.//@base` returns attribute values + - Test namespace handling in XPath +2. Create `DomDatatypeContentTest` that tests `JDom2DatatypeContent`: + - Test serialization of JDOM2 element to XMLStreamWriter produces expected XML + - Test multiple elements serialization + - Test dependency list handling +3. **Verify tests PASS** with current JDOM2 implementation + +**Step 2: Create new implementations that pass the same tests** + +4. Create `XmlSchemaLoader` using standard `DocumentBuilderFactory` and `javax.xml.xpath.XPath` +5. Create `DomDatatypeContent` using `org.w3c.dom.Element` and `Transformer` +6. Update tests to use new implementations (or parameterize to test both) +7. **Verify tests PASS** with new implementations + +**Step 3: Switch over and clean up** + +8. Update provider classes to use new implementations +9. Delete JDOM2 classes after all tests confirm equivalence + +#### Acceptance Criteria + +- [ ] Create `XmlSchemaLoaderTest` with characterization tests for existing JDOM2 behavior +- [ ] Create `DomDatatypeContentTest` with characterization tests for existing JDOM2 behavior +- [ ] Verify characterization tests PASS with JDOM2 implementation +- [ ] Create `XmlSchemaLoader` using `DocumentBuilderFactory` and `javax.xml.xpath.XPath` +- [ ] Create `DomDatatypeContent` using `org.w3c.dom.Element` and `Transformer` +- [ ] Verify tests PASS with new implementations (behavioral equivalence confirmed) +- [ ] Update `AbstractXmlDatatypeProvider` to use new loader class +- [ ] Update `XmlCoreDatatypeProvider` with standard XPath for `.//@base` query +- [ ] Update `AbstractXmlMarkupDatatypeProvider` with standard DOM element handling +- [ ] Update `XmlProseBaseDatatypeProvider` if needed +- [ ] Delete `JDom2XmlSchemaLoader.java` +- [ ] Delete `JDom2DatatypeContent.java` +- [ ] All schemagen tests pass + +--- + +### Phase 3: Update XmlSchemaGenerator + +Replace Saxon XSLT transformation with IndentingXMLStreamWriter. + +#### Files Changed + +| File | Change Type | +|------|-------------| +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java` | Modify | +| `schemagen/src/main/resources/identity.xsl` | Delete | + +#### TDD Sequence (Characterization Tests First) + +**Step 1: Verify existing integration tests capture current behavior** + +1. Run existing `XmlSuiteTest` and other schema generation tests +2. Verify tests capture that generated schemas are: + - Properly indented + - Semantically correct + - Contain expected elements and structure +3. If coverage is insufficient, add characterization tests for schema output format + +**Step 2: Replace Saxon with IndentingXMLStreamWriter** + +4. Modify `newWriter` to wrap `XMLStreamWriter2` with `IndentingXMLStreamWriter` +5. Remove the `generateFromModule` override that uses XSLT post-processing +6. Remove Saxon imports +7. **Verify existing tests PASS** - schemas still properly indented + +**Step 3: Clean up** + +8. Delete `identity.xsl` resource file + +#### Acceptance Criteria + +- [ ] Verify existing schema generation tests pass (baseline) +- [ ] Wrap `XMLStreamWriter2` with `IndentingXMLStreamWriter` in `newWriter` method +- [ ] Remove `generateFromModule` override that uses XSLT post-processing +- [ ] Remove Saxon imports +- [ ] Verify existing tests still pass (behavioral equivalence) +- [ ] Delete `identity.xsl` resource file +- [ ] Verify generated schemas are properly indented + +--- + +### Phase 4: Update Module and Build Configuration + +Remove dependencies from module-info.java and pom.xml files. + +#### Files Changed + +| File | Change Type | +|------|-------------| +| `schemagen/src/main/java/module-info.java` | Modify | +| `schemagen/pom.xml` | Modify | +| `pom.xml` | Modify | +| `THIRD_PARTY_LICENSES.md` | Modify | + +#### Acceptance Criteria + +- [ ] Remove `requires Saxon.HE;` from module-info.java +- [ ] Remove `requires org.jdom2;` from module-info.java +- [ ] Remove Saxon-HE dependency from schemagen/pom.xml +- [ ] Remove jdom2 dependency from schemagen/pom.xml +- [ ] Remove jaxen dependency from schemagen/pom.xml +- [ ] Remove Saxon-HE from root pom.xml dependencyManagement +- [ ] Remove xmlresolver from root pom.xml dependencyManagement (both library and data artifacts) +- [ ] Remove jdom2 from root pom.xml dependencyManagement +- [ ] Remove jaxen from root pom.xml dependencyManagement +- [ ] Remove version properties for removed dependencies (`dependency.saxon.version`, `dependency.xmlresolver.version`) +- [ ] Update THIRD_PARTY_LICENSES.md to remove Saxon and xmlresolver entries + +--- + +### Phase 5: Update Tests + +Update any tests that depend on JDOM2 or have whitespace-sensitive assertions. + +#### Files Changed + +| File | Change Type | +|------|-------------| +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/XmlSuiteTest.java` | Modify | + +#### Acceptance Criteria + +- [ ] Update `XmlSuiteTest` to use standard DOM/XPath instead of JDOM2 +- [ ] Replace `StAXEventBuilder` with `DocumentBuilder` +- [ ] Replace JDOM2 XPath with `javax.xml.xpath.XPath` +- [ ] Verify all existing tests pass +- [ ] Add tests for `IndentingXMLStreamWriter` +- [ ] Add tests for `XmlSchemaLoader` + +--- + +### Phase 6: Final Verification + +#### Acceptance Criteria + +- [ ] Run `mvn clean install -PCI -Prelease` - all checks pass +- [ ] Verify no Saxon or JDOM2 classes in compiled output +- [ ] Compare generated schema output before/after (semantic equivalence) +- [ ] Update PRD status in CLAUDE.md + +--- + +## Files Changed Summary + +### Test Files (TDD - Written First) + +| File | Change Type | +|------|-------------| +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java` | Add | +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java` | Add | +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContentTest.java` | Add | +| `schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/XmlSuiteTest.java` | Modify | + +### Implementation Files + +| File | Change Type | +|------|-------------| +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContent.java` | Add | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2XmlSchemaLoader.java` | Delete | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2DatatypeContent.java` | Delete | +| `schemagen/src/main/resources/identity.xsl` | Delete | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlDatatypeProvider.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlCoreDatatypeProvider.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlMarkupDatatypeProvider.java` | Modify | +| `schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlProseBaseDatatypeProvider.java` | Modify | + +### Configuration Files + +| File | Change Type | +|------|-------------| +| `schemagen/src/main/java/module-info.java` | Modify | +| `schemagen/pom.xml` | Modify | +| `pom.xml` | Modify | +| `THIRD_PARTY_LICENSES.md` | Modify | + +**Total files**: 19 (6 add, 3 delete, 10 modify) + +--- + +## Technical Notes + +### IndentingXMLStreamWriter Design + +```java +public class IndentingXMLStreamWriter implements XMLStreamWriter { + private final XMLStreamWriter delegate; + private int depth = 0; + private final Deque hasTextStack = new ArrayDeque<>(); + private boolean hasText = false; // Current element's mixed content state + private static final String INDENT = " "; + private static final String NEWLINE = "\n"; + + // State tracking: + // - depth: current nesting level for indentation + // - hasTextStack: stack of hasText values for ancestor elements + // - hasText: true if current element contains text (mixed content mode) + // When hasText is true, suppress indentation to preserve text formatting + + // Key methods: + // - writeStartElement: + // if (!hasText) write newline + indent + // hasTextStack.push(hasText) // save parent's state + // delegate.writeStartElement(...) + // depth++ + // hasText = false // reset for new element + // + // - writeEndElement: + // depth-- + // if (!hasText) write newline + indent + // delegate.writeEndElement() + // hasText = hasTextStack.pop() // restore parent's state + // + // - writeCharacters: + // if (text is not whitespace-only) hasText = true + // delegate.writeCharacters(...) + // + // - writeCData: + // hasText = true // CDATA is text content + // delegate.writeCData(...) + // + // - writeComment: + // if (!hasText) write newline + indent + // delegate.writeComment(...) + // + // - writeProcessingInstruction: + // if (!hasText) write newline + indent + // delegate.writeProcessingInstruction(...) +} +``` + +**Stack-based parent state tracking:** + +The `hasTextStack` preserves mixed content state across nested elements: + +```text + hasText=false, stack=[] + Some text hasText=true, stack=[] + hasText=true, stack=[true] (parent had text) + More text hasText=true, stack=[true] + hasText=true, stack=[true,true] + bold hasText=true, stack=[true,true] + pop → hasText=true + pop → hasText=true + no pop (root level) +``` + +This ensures that once text is written in an ancestor, all descendants suppress indentation. + +**Critical behavior for mixed content:** + +Input: `

Some text with bold words

` + +- When `writeCharacters("Some text with ")` is called, set `hasText = true` +- When `writeStartElement("b")` is called, do NOT indent (hasText is true) +- When `writeEndElement()` for `b` is called, do NOT indent +- When `writeCharacters(" words")` is called, continue in text mode +- When `writeEndElement()` for `p` is called, do NOT indent + +This preserves: `

Some text with bold words

` without spurious whitespace. + +### XPath Namespace Handling + +Standard Java XPath requires a `NamespaceContext` implementation: + +```java +XPath xpath = XPathFactory.newInstance().newXPath(); +xpath.setNamespaceContext(new NamespaceContext() { + @Override + public String getNamespaceURI(String prefix) { + if ("xs".equals(prefix)) { + return "http://www.w3.org/2001/XMLSchema"; + } + return XMLConstants.NULL_NS_URI; + } + // ... other methods +}); +``` + +### DOM Element Serialization + +To write DOM elements to XMLStreamWriter: + +```java +Transformer transformer = TransformerFactory.newInstance().newTransformer(); +transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); +transformer.transform(new DOMSource(element), new StAXResult(writer)); +``` + +--- + +## Rollback Plan + +If issues are discovered after merge: +1. Revert the PR commit +2. Re-add dependencies to pom.xml files +3. Restore deleted files from git history From 19f2f56f71bdd08813ca20c32291ecca7b883d59 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 11:09:23 -0500 Subject: [PATCH 2/7] refactor: remove Saxon-HE, JDOM2, and jaxen dependencies from schemagen Replace heavy XML processing dependencies with standard Java APIs: - Add IndentingXMLStreamWriter for XML output formatting, replacing Saxon XSLT post-processing with identity.xsl - Add XmlSchemaLoader using standard DOM/XPath APIs to replace JDom2XmlSchemaLoader - Add DomDatatypeContent to write DOM elements to XMLStreamWriter, replacing JDom2DatatypeContent - Update XmlSchemaGenerator to wrap writer with IndentingXMLStreamWriter2 - Update all datatype providers to use standard DOM APIs - Update XmlSuiteTest to use standard DOM/XPath Dependencies removed from schemagen module: - net.sf.saxon:Saxon-HE - org.jdom:jdom2 - jaxen:jaxen This reduces the dependency footprint and eliminates automodule warnings for Saxon-HE in the build output. --- schemagen/pom.xml | 14 - .../schemagen/xml/XmlSchemaGenerator.java | 51 +- .../xml/impl/AbstractXmlDatatypeProvider.java | 10 +- .../AbstractXmlMarkupDatatypeProvider.java | 10 +- .../xml/impl/DomDatatypeContent.java | 172 ++++++ .../xml/impl/IndentingXMLStreamWriter.java | 295 +++++++++ .../xml/impl/IndentingXMLStreamWriter2.java | 318 ++++++++++ .../xml/impl/JDom2DatatypeContent.java | 72 --- .../xml/impl/JDom2XmlSchemaLoader.java | 115 ---- .../xml/impl/XmlCoreDatatypeProvider.java | 48 +- .../impl/XmlProseBaseDatatypeProvider.java | 11 +- .../schemagen/xml/impl/XmlSchemaLoader.java | 206 +++++++ schemagen/src/main/java/module-info.java | 4 +- schemagen/src/main/resources/identity.xsl | 26 - .../metaschema/schemagen/XmlSuiteTest.java | 77 ++- .../xml/impl/DomDatatypeContentTest.java | 254 ++++++++ .../impl/IndentingXMLStreamWriterTest.java | 565 ++++++++++++++++++ .../xml/impl/XmlSchemaLoaderTest.java | 185 ++++++ 18 files changed, 2099 insertions(+), 334 deletions(-) create mode 100644 schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContent.java create mode 100644 schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter.java create mode 100644 schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java delete mode 100644 schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2DatatypeContent.java delete mode 100644 schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2XmlSchemaLoader.java create mode 100644 schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java delete mode 100644 schemagen/src/main/resources/identity.xsl create mode 100644 schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContentTest.java create mode 100644 schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java create mode 100644 schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java diff --git a/schemagen/pom.xml b/schemagen/pom.xml index 790263c11..c2d8b0b27 100644 --- a/schemagen/pom.xml +++ b/schemagen/pom.xml @@ -49,23 +49,9 @@ org.apache.logging.log4j log4j-jul - - org.jdom - jdom2 - - - - jaxen - jaxen - org.codehaus.woodstox stax2-api - - - net.sf.saxon - Saxon-HE - \ No newline at end of file diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java index f82ea5a17..1178c9aa6 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java @@ -17,6 +17,7 @@ import gov.nist.secauto.metaschema.schemagen.AbstractSchemaGenerator; import gov.nist.secauto.metaschema.schemagen.SchemaGenerationException; import gov.nist.secauto.metaschema.schemagen.SchemaGenerationFeature; +import gov.nist.secauto.metaschema.schemagen.xml.impl.IndentingXMLStreamWriter2; import gov.nist.secauto.metaschema.schemagen.xml.impl.XmlDatatypeManager; import gov.nist.secauto.metaschema.schemagen.xml.impl.XmlGenerationState; import gov.nist.secauto.metaschema.schemagen.xml.impl.schematype.IXmlType; @@ -25,10 +26,6 @@ import org.codehaus.stax2.XMLStreamWriter2; import org.eclipse.jdt.annotation.Owning; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.List; @@ -37,13 +34,6 @@ import javax.xml.namespace.QName; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; -import javax.xml.transform.Source; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.stream.StreamSource; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -126,7 +116,9 @@ protected AutoCloser newWriter( Writer out) { XMLStreamWriter2 writer; try { - writer = ObjectUtils.notNull((XMLStreamWriter2) getXmlOutputFactory().createXMLStreamWriter(out)); + XMLStreamWriter2 baseWriter + = ObjectUtils.notNull((XMLStreamWriter2) getXmlOutputFactory().createXMLStreamWriter(out)); + writer = new IndentingXMLStreamWriter2(baseWriter); } catch (XMLStreamException ex) { throw new SchemaGenerationException(ex); } @@ -147,41 +139,6 @@ protected XmlGenerationState newGenerationState( return new XmlGenerationState(module, schemaWriter, configuration); } - @Override - public void generateFromModule( - @NonNull IModule module, - @NonNull Writer out, - @NonNull IConfiguration> configuration) { - // super.generateFromModule(module, out, configuration); - - String generatedSchema; - try (StringWriter stringWriter = new StringWriter()) { - super.generateFromModule(module, stringWriter, configuration); - generatedSchema = stringWriter.toString(); - } catch (IOException ex) { - throw new SchemaGenerationException(ex); - } - - try (InputStream is = getClass().getResourceAsStream("/identity.xsl")) { - Source xsltSource = new StreamSource(is); - - // TransformerFactory transformerFactory = TransformerFactory.newInstance(); - TransformerFactory transformerFactory = new net.sf.saxon.TransformerFactoryImpl(); - Transformer transformer = transformerFactory.newTransformer(xsltSource); - - try (StringReader stringReader = new StringReader(generatedSchema)) { - Source xmlSource = new StreamSource(stringReader); - - StreamResult result = new StreamResult(out); - transformer.transform(xmlSource, result); - } catch (TransformerException ex) { - throw new SchemaGenerationException(ex); - } - } catch (IOException | TransformerConfigurationException ex) { - throw new SchemaGenerationException(ex); - } - } - @Override protected void generateSchema(XmlGenerationState state) { diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlDatatypeProvider.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlDatatypeProvider.java index 0f4384000..c98dff718 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlDatatypeProvider.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlDatatypeProvider.java @@ -7,8 +7,8 @@ import org.codehaus.stax2.XMLStreamWriter2; import org.eclipse.jdt.annotation.Owning; -import org.jdom2.Element; -import org.jdom2.JDOMException; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; import java.io.IOException; import java.io.InputStream; @@ -49,12 +49,12 @@ private void initSchema() { if (datatypes == null) { try (InputStream is = getSchemaResource()) { assert is != null; - JDom2XmlSchemaLoader loader = new JDom2XmlSchemaLoader(is); + XmlSchemaLoader loader = new XmlSchemaLoader(is); List elements = queryElements(loader); datatypes = Collections.unmodifiableMap(handleResults(elements)); - } catch (JDOMException | IOException ex) { + } catch (SAXException | IOException ex) { throw new IllegalStateException(ex); } } @@ -69,7 +69,7 @@ private void initSchema() { * @return the list of elements representing datatype definitions */ @NonNull - protected abstract List queryElements(JDom2XmlSchemaLoader loader); + protected abstract List queryElements(XmlSchemaLoader loader); /** * Process the queried elements and create datatype content mappings. diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlMarkupDatatypeProvider.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlMarkupDatatypeProvider.java index 9274e8125..aa3ed2993 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlMarkupDatatypeProvider.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/AbstractXmlMarkupDatatypeProvider.java @@ -10,7 +10,7 @@ import gov.nist.secauto.metaschema.core.util.ObjectUtils; import org.eclipse.jdt.annotation.Owning; -import org.jdom2.Element; +import org.w3c.dom.Element; import java.io.InputStream; import java.util.List; @@ -44,10 +44,10 @@ protected InputStream getSchemaResource() { protected abstract String getSchemaResourcePath(); @Override - protected List queryElements(JDom2XmlSchemaLoader loader) { + protected List queryElements(XmlSchemaLoader loader) { return loader.getContent( "/xs:schema/*", - CollectionUtil.singletonMap("xs", JDom2XmlSchemaLoader.NS_XML_SCHEMA)); + CollectionUtil.singletonMap("xs", XmlSchemaLoader.NS_XML_SCHEMA)); } /** @@ -63,10 +63,10 @@ protected Map handleResults(@NonNull List ite String dataTypeName = getDataTypeName(); return CollectionUtil.singletonMap( dataTypeName, - new JDom2DatatypeContent( + new DomDatatypeContent( dataTypeName, ObjectUtils.notNull(items.stream() - .filter(element -> !"include".equals(element.getName())) + .filter(element -> !"include".equals(element.getLocalName())) .collect(Collectors.toList())), CollectionUtil.emptyList())); } diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContent.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContent.java new file mode 100644 index 000000000..a59b05e20 --- /dev/null +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContent.java @@ -0,0 +1,172 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import gov.nist.secauto.metaschema.core.util.CollectionUtil; + +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Represents datatype content backed by standard DOM elements. + *

+ * This class stores XML Schema datatype definitions as DOM elements and + * provides the capability to write them to an XML stream. It replaces the + * JDOM2-based implementation with standard Java DOM APIs. + */ +public class DomDatatypeContent + extends AbstractDatatypeContent { + + @NonNull + private final List content; + + /** + * Constructs a new DOM-backed datatype content instance. + * + * @param typeName + * the name of the datatype + * @param content + * the list of DOM elements representing the datatype definition + * @param dependencies + * the list of datatype names that this datatype depends on + */ + public DomDatatypeContent( + @NonNull String typeName, + @NonNull List content, + @NonNull List dependencies) { + super(typeName, dependencies); + this.content = CollectionUtil.unmodifiableList(new ArrayList<>(content)); + } + + /** + * Retrieves the DOM elements representing the datatype content. + * + * @return an unmodifiable list of DOM elements + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + protected List getContent() { + return content; + } + + @Override + public void write(@NonNull XMLStreamWriter writer) throws XMLStreamException { + for (Element element : getContent()) { + writeElement(element, writer); + } + } + + /** + * Writes a DOM element and its contents to the XMLStreamWriter. + * + * @param element + * the DOM element to write + * @param writer + * the XMLStreamWriter to write to + * @throws XMLStreamException + * if an error occurs during writing + */ + private void writeElement(@NonNull Element element, @NonNull XMLStreamWriter writer) throws XMLStreamException { + String namespaceURI = element.getNamespaceURI(); + String localName = element.getLocalName(); + String prefix = element.getPrefix(); + + // Write the start element + if (namespaceURI != null && !namespaceURI.isEmpty()) { + if (prefix != null && !prefix.isEmpty()) { + writer.writeStartElement(prefix, localName, namespaceURI); + } else { + writer.writeStartElement(namespaceURI, localName); + } + } else { + writer.writeStartElement(localName != null ? localName : element.getTagName()); + } + + // Write namespace declarations if this element has them + if (namespaceURI != null && !namespaceURI.isEmpty()) { + String existingPrefix = writer.getPrefix(namespaceURI); + if (existingPrefix == null) { + if (prefix != null && !prefix.isEmpty()) { + writer.writeNamespace(prefix, namespaceURI); + } else { + writer.writeDefaultNamespace(namespaceURI); + } + } + } + + // Write attributes + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node attr = attributes.item(i); + String attrName = attr.getNodeName(); + String attrValue = attr.getNodeValue(); + + // Skip xmlns declarations - they're handled separately + if (attrName.startsWith("xmlns")) { + continue; + } + + String attrNamespaceURI = attr.getNamespaceURI(); + String attrLocalName = attr.getLocalName(); + String attrPrefix = attr.getPrefix(); + + if (attrNamespaceURI != null && !attrNamespaceURI.isEmpty()) { + if (attrPrefix != null && !attrPrefix.isEmpty()) { + writer.writeAttribute(attrPrefix, attrNamespaceURI, attrLocalName, attrValue); + } else { + writer.writeAttribute(attrNamespaceURI, attrLocalName, attrValue); + } + } else { + writer.writeAttribute(attrLocalName != null ? attrLocalName : attrName, attrValue); + } + } + + // Write child nodes + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + switch (child.getNodeType()) { + case Node.ELEMENT_NODE: + writeElement((Element) child, writer); + break; + case Node.TEXT_NODE: + String text = child.getNodeValue(); + if (text != null && !text.isEmpty()) { + writer.writeCharacters(text); + } + break; + case Node.CDATA_SECTION_NODE: + String cdata = child.getNodeValue(); + if (cdata != null) { + writer.writeCData(cdata); + } + break; + case Node.COMMENT_NODE: + String comment = child.getNodeValue(); + if (comment != null) { + writer.writeComment(comment); + } + break; + default: + // Ignore other node types + break; + } + } + + // Write end element + writer.writeEndElement(); + } +} diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter.java new file mode 100644 index 000000000..dd7eb1c36 --- /dev/null +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter.java @@ -0,0 +1,295 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import java.util.ArrayDeque; +import java.util.Deque; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * An XMLStreamWriter wrapper that adds indentation to the output. + *

+ * This wrapper handles mixed content correctly by tracking when text has been + * written to an element. When an element contains text (mixed content), no + * indentation is added to preserve the text formatting. + *

+ * This class is used to replace Saxon XSLT post-processing for schema + * indentation, providing streaming indentation without buffering the entire + * document. + */ +public class IndentingXMLStreamWriter implements XMLStreamWriter, AutoCloseable { + + private static final String NEWLINE = "\n"; + private static final String INDENT = " "; + + @NonNull + private final XMLStreamWriter delegate; + + private int depth; + private final Deque hasTextStack = new ArrayDeque<>(); + private boolean hasText; + private boolean lastWasStart; + + /** + * Constructs a new indenting XML stream writer. + * + * @param delegate + * the underlying writer to delegate to + */ + public IndentingXMLStreamWriter(@NonNull XMLStreamWriter delegate) { + this.delegate = delegate; + this.depth = 0; + this.hasText = false; + this.lastWasStart = false; + } + + /** + * Writes indentation at the current depth level. + * + * @throws XMLStreamException + * if an error occurs writing + */ + private void writeIndent() throws XMLStreamException { + delegate.writeCharacters(NEWLINE); + for (int i = 0; i < depth; i++) { + delegate.writeCharacters(INDENT); + } + } + + @Override + public void writeStartElement(String localName) throws XMLStreamException { + prepareStartElement(); + delegate.writeStartElement(localName); + afterStartElement(); + } + + @Override + public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException { + prepareStartElement(); + delegate.writeStartElement(namespaceURI, localName); + afterStartElement(); + } + + @Override + public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + prepareStartElement(); + delegate.writeStartElement(prefix, localName, namespaceURI); + afterStartElement(); + } + + /** + * Prepares for writing a start element by adding indentation if appropriate. + * + * @throws XMLStreamException + * if an error occurs writing + */ + private void prepareStartElement() throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + hasTextStack.push(hasText); + } + + /** + * Updates state after writing a start element. + */ + private void afterStartElement() { + depth++; + hasText = false; + lastWasStart = true; + } + + @Override + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + delegate.writeEmptyElement(namespaceURI, localName); + lastWasStart = false; + } + + @Override + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + delegate.writeEmptyElement(prefix, localName, namespaceURI); + lastWasStart = false; + } + + @Override + public void writeEmptyElement(String localName) throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + delegate.writeEmptyElement(localName); + lastWasStart = false; + } + + @Override + public void writeEndElement() throws XMLStreamException { + depth--; + boolean parentHasText = hasTextStack.isEmpty() ? false : hasTextStack.pop(); + + if (!hasText && !lastWasStart) { + writeIndent(); + } + delegate.writeEndElement(); + hasText = parentHasText; + lastWasStart = false; + } + + @Override + public void writeEndDocument() throws XMLStreamException { + delegate.writeEndDocument(); + } + + @Override + public void close() throws XMLStreamException { + delegate.close(); + } + + @Override + public void flush() throws XMLStreamException { + delegate.flush(); + } + + @Override + public void writeAttribute(String localName, String value) throws XMLStreamException { + delegate.writeAttribute(localName, value); + } + + @Override + public void writeAttribute(String prefix, String namespaceURI, String localName, String value) + throws XMLStreamException { + delegate.writeAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeAttribute(String namespaceURI, String localName, String value) throws XMLStreamException { + delegate.writeAttribute(namespaceURI, localName, value); + } + + @Override + public void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException { + delegate.writeNamespace(prefix, namespaceURI); + } + + @Override + public void writeDefaultNamespace(String namespaceURI) throws XMLStreamException { + delegate.writeDefaultNamespace(namespaceURI); + } + + @Override + public void writeComment(String data) throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + delegate.writeComment(data); + lastWasStart = false; + } + + @Override + public void writeProcessingInstruction(String target) throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + delegate.writeProcessingInstruction(target); + lastWasStart = false; + } + + @Override + public void writeProcessingInstruction(String target, String data) throws XMLStreamException { + if (!hasText) { + writeIndent(); + } + delegate.writeProcessingInstruction(target, data); + lastWasStart = false; + } + + @Override + public void writeCData(String data) throws XMLStreamException { + delegate.writeCData(data); + hasText = true; + lastWasStart = false; + } + + @Override + public void writeDTD(String dtd) throws XMLStreamException { + delegate.writeDTD(dtd); + } + + @Override + public void writeEntityRef(String name) throws XMLStreamException { + delegate.writeEntityRef(name); + hasText = true; + lastWasStart = false; + } + + @Override + public void writeStartDocument() throws XMLStreamException { + delegate.writeStartDocument(); + } + + @Override + public void writeStartDocument(String version) throws XMLStreamException { + delegate.writeStartDocument(version); + } + + @Override + public void writeStartDocument(String encoding, String version) throws XMLStreamException { + delegate.writeStartDocument(encoding, version); + } + + @Override + public void writeCharacters(String text) throws XMLStreamException { + delegate.writeCharacters(text); + hasText = true; + lastWasStart = false; + } + + @Override + public void writeCharacters(char[] text, int start, int len) throws XMLStreamException { + delegate.writeCharacters(text, start, len); + hasText = true; + lastWasStart = false; + } + + @Override + public String getPrefix(String uri) throws XMLStreamException { + return delegate.getPrefix(uri); + } + + @Override + public void setPrefix(String prefix, String uri) throws XMLStreamException { + delegate.setPrefix(prefix, uri); + } + + @Override + public void setDefaultNamespace(String uri) throws XMLStreamException { + delegate.setDefaultNamespace(uri); + } + + @Override + public void setNamespaceContext(NamespaceContext context) throws XMLStreamException { + delegate.setNamespaceContext(context); + } + + @Override + public NamespaceContext getNamespaceContext() { + return delegate.getNamespaceContext(); + } + + @Override + public Object getProperty(String name) { + return delegate.getProperty(name); + } +} diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java new file mode 100644 index 000000000..da53a86ee --- /dev/null +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java @@ -0,0 +1,318 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import org.codehaus.stax2.XMLStreamLocation2; +import org.codehaus.stax2.XMLStreamReader2; +import org.codehaus.stax2.XMLStreamWriter2; +import org.codehaus.stax2.validation.ValidationProblemHandler; +import org.codehaus.stax2.validation.XMLValidationSchema; +import org.codehaus.stax2.validation.XMLValidator; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import javax.xml.stream.XMLStreamException; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * An XMLStreamWriter2 wrapper that adds indentation to the output. + *

+ * This wrapper extends {@link IndentingXMLStreamWriter} and implements + * {@link XMLStreamWriter2} by delegating the Stax2-specific methods to the + * underlying writer. + */ +public class IndentingXMLStreamWriter2 + extends IndentingXMLStreamWriter + implements XMLStreamWriter2 { + + @NonNull + private final XMLStreamWriter2 delegate2; + + /** + * Constructs a new indenting XML stream writer for XMLStreamWriter2. + * + * @param delegate + * the underlying XMLStreamWriter2 to delegate to + */ + public IndentingXMLStreamWriter2(@NonNull XMLStreamWriter2 delegate) { + super(delegate); + this.delegate2 = delegate; + } + + /** + * Gets the underlying XMLStreamWriter2. + * + * @return the delegate writer + */ + protected XMLStreamWriter2 getDelegate2() { + return delegate2; + } + + // ============================================================ + // XMLStreamWriter2-specific methods - delegate to underlying writer + // ============================================================ + + @Override + public boolean isPropertySupported(String name) { + return delegate2.isPropertySupported(name); + } + + @Override + public boolean setProperty(String name, Object value) { + return delegate2.setProperty(name, value); + } + + @Override + public XMLStreamLocation2 getLocation() { + return delegate2.getLocation(); + } + + @Override + public String getEncoding() { + return delegate2.getEncoding(); + } + + @Override + public void writeSpace(String text) throws XMLStreamException { + delegate2.writeSpace(text); + } + + @Override + public void writeSpace(char[] text, int offset, int length) throws XMLStreamException { + delegate2.writeSpace(text, offset, length); + } + + @Override + public void writeRaw(String text) throws XMLStreamException { + delegate2.writeRaw(text); + } + + @Override + public void writeRaw(String text, int offset, int length) throws XMLStreamException { + delegate2.writeRaw(text, offset, length); + } + + @Override + public void writeRaw(char[] text, int offset, int length) throws XMLStreamException { + delegate2.writeRaw(text, offset, length); + } + + @Override + public void copyEventFromReader(XMLStreamReader2 reader, boolean preserveEventData) throws XMLStreamException { + delegate2.copyEventFromReader(reader, preserveEventData); + } + + @Override + public void closeCompletely() throws XMLStreamException { + delegate2.closeCompletely(); + } + + @Override + public void writeDTD(String rootName, String systemId, String publicId, String internalSubset) + throws XMLStreamException { + delegate2.writeDTD(rootName, systemId, publicId, internalSubset); + } + + @Override + public void writeFullEndElement() throws XMLStreamException { + delegate2.writeFullEndElement(); + } + + @Override + public void writeStartDocument(String version, String encoding, boolean standAlone) throws XMLStreamException { + delegate2.writeStartDocument(version, encoding, standAlone); + } + + @Override + public void writeCData(char[] text, int start, int len) throws XMLStreamException { + delegate2.writeCData(text, start, len); + } + + // ============================================================ + // TypedXMLStreamWriter methods - delegate to underlying writer + // ============================================================ + + @Override + public void writeBoolean(boolean value) throws XMLStreamException { + delegate2.writeBoolean(value); + } + + @Override + public void writeInt(int value) throws XMLStreamException { + delegate2.writeInt(value); + } + + @Override + public void writeLong(long value) throws XMLStreamException { + delegate2.writeLong(value); + } + + @Override + public void writeFloat(float value) throws XMLStreamException { + delegate2.writeFloat(value); + } + + @Override + public void writeDouble(double value) throws XMLStreamException { + delegate2.writeDouble(value); + } + + @Override + public void writeInteger(BigInteger value) throws XMLStreamException { + delegate2.writeInteger(value); + } + + @Override + public void writeDecimal(BigDecimal value) throws XMLStreamException { + delegate2.writeDecimal(value); + } + + @Override + public void writeQName(javax.xml.namespace.QName name) throws XMLStreamException { + delegate2.writeQName(name); + } + + @Override + public void writeBinary(byte[] value, int from, int length) throws XMLStreamException { + delegate2.writeBinary(value, from, length); + } + + @Override + public void writeBinary(org.codehaus.stax2.typed.Base64Variant variant, byte[] value, int from, int length) + throws XMLStreamException { + delegate2.writeBinary(variant, value, from, length); + } + + @Override + public void writeIntArray(int[] value, int from, int length) throws XMLStreamException { + delegate2.writeIntArray(value, from, length); + } + + @Override + public void writeLongArray(long[] value, int from, int length) throws XMLStreamException { + delegate2.writeLongArray(value, from, length); + } + + @Override + public void writeFloatArray(float[] value, int from, int length) throws XMLStreamException { + delegate2.writeFloatArray(value, from, length); + } + + @Override + public void writeDoubleArray(double[] value, int from, int length) throws XMLStreamException { + delegate2.writeDoubleArray(value, from, length); + } + + @Override + public void writeBooleanAttribute(String prefix, String namespaceURI, String localName, boolean value) + throws XMLStreamException { + delegate2.writeBooleanAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeIntAttribute(String prefix, String namespaceURI, String localName, int value) + throws XMLStreamException { + delegate2.writeIntAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeLongAttribute(String prefix, String namespaceURI, String localName, long value) + throws XMLStreamException { + delegate2.writeLongAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeFloatAttribute(String prefix, String namespaceURI, String localName, float value) + throws XMLStreamException { + delegate2.writeFloatAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeDoubleAttribute(String prefix, String namespaceURI, String localName, double value) + throws XMLStreamException { + delegate2.writeDoubleAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeIntegerAttribute(String prefix, String namespaceURI, String localName, BigInteger value) + throws XMLStreamException { + delegate2.writeIntegerAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeDecimalAttribute(String prefix, String namespaceURI, String localName, BigDecimal value) + throws XMLStreamException { + delegate2.writeDecimalAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeQNameAttribute(String prefix, String namespaceURI, String localName, javax.xml.namespace.QName name) + throws XMLStreamException { + delegate2.writeQNameAttribute(prefix, namespaceURI, localName, name); + } + + @Override + public void writeBinaryAttribute(String prefix, String namespaceURI, String localName, byte[] value) + throws XMLStreamException { + delegate2.writeBinaryAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeBinaryAttribute(org.codehaus.stax2.typed.Base64Variant variant, String prefix, String namespaceURI, + String localName, byte[] value) throws XMLStreamException { + delegate2.writeBinaryAttribute(variant, prefix, namespaceURI, localName, value); + } + + @Override + public void writeIntArrayAttribute(String prefix, String namespaceURI, String localName, int[] value) + throws XMLStreamException { + delegate2.writeIntArrayAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeLongArrayAttribute(String prefix, String namespaceURI, String localName, long[] value) + throws XMLStreamException { + delegate2.writeLongArrayAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeFloatArrayAttribute(String prefix, String namespaceURI, String localName, float[] value) + throws XMLStreamException { + delegate2.writeFloatArrayAttribute(prefix, namespaceURI, localName, value); + } + + @Override + public void writeDoubleArrayAttribute(String prefix, String namespaceURI, String localName, double[] value) + throws XMLStreamException { + delegate2.writeDoubleArrayAttribute(prefix, namespaceURI, localName, value); + } + + // ============================================================ + // Validatable methods - delegate to underlying writer + // ============================================================ + + @Override + public XMLValidator validateAgainst(XMLValidationSchema schema) throws XMLStreamException { + return delegate2.validateAgainst(schema); + } + + @Override + public XMLValidator stopValidatingAgainst(XMLValidationSchema schema) throws XMLStreamException { + return delegate2.stopValidatingAgainst(schema); + } + + @Override + public XMLValidator stopValidatingAgainst(XMLValidator validator) throws XMLStreamException { + return delegate2.stopValidatingAgainst(validator); + } + + @Override + public ValidationProblemHandler setValidationProblemHandler(ValidationProblemHandler handler) { + return delegate2.setValidationProblemHandler(handler); + } +} diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2DatatypeContent.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2DatatypeContent.java deleted file mode 100644 index 35fb9c2b0..000000000 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2DatatypeContent.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: none - * SPDX-License-Identifier: CC0-1.0 - */ - -package gov.nist.secauto.metaschema.schemagen.xml.impl; - -import gov.nist.secauto.metaschema.core.util.CollectionUtil; - -import org.jdom2.Element; -import org.jdom2.output.Format; -import org.jdom2.output.StAXStreamOutputter; - -import java.util.ArrayList; -import java.util.List; - -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; - -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Represents datatype content backed by JDOM2 elements. - *

- * This class stores XML Schema datatype definitions as JDOM2 elements and - * provides the capability to write them to an XML stream. - */ -public class JDom2DatatypeContent - extends AbstractDatatypeContent { - - @NonNull - private final List content; - - /** - * Constructs a new JDOM2-backed datatype content instance. - * - * @param typeName - * the name of the datatype - * @param content - * the list of JDOM2 elements representing the datatype definition - * @param dependencies - * the list of datatype names that this datatype depends on - */ - public JDom2DatatypeContent( - @NonNull String typeName, - @NonNull List content, - @NonNull List dependencies) { - super(typeName, dependencies); - this.content = CollectionUtil.unmodifiableList(new ArrayList<>(content)); - } - - /** - * Retrieves the JDOM2 elements representing the datatype content. - * - * @return an unmodifiable list of JDOM2 elements - */ - protected List getContent() { - return content; - } - - @Override - public void write(@NonNull XMLStreamWriter writer) throws XMLStreamException { - Format format = Format.getRawFormat(); - format.setOmitDeclaration(true); - - StAXStreamOutputter out = new StAXStreamOutputter(format); - - for (Element content : getContent()) { - out.output(content, writer); - } - } -} diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2XmlSchemaLoader.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2XmlSchemaLoader.java deleted file mode 100644 index c985aa594..000000000 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/JDom2XmlSchemaLoader.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * SPDX-FileCopyrightText: none - * SPDX-License-Identifier: CC0-1.0 - */ - -package gov.nist.secauto.metaschema.schemagen.xml.impl; - -import org.jdom2.Document; -import org.jdom2.Element; -import org.jdom2.JDOMException; -import org.jdom2.Namespace; -import org.jdom2.filter.Filters; -import org.jdom2.input.SAXBuilder; -import org.jdom2.xpath.XPathExpression; -import org.jdom2.xpath.XPathFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Loads and queries XML Schema documents using JDOM2. - *

- * This class provides functionality to load XML Schema documents from various - * sources and query their content using XPath expressions. - */ -public class JDom2XmlSchemaLoader { - /** The XML Schema namespace URI. */ - @NonNull - public static final String NS_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; - - @NonNull - private final Document document; - - /** - * Constructs a new XML Schema loader from a file path. - * - * @param path - * the path to the XML Schema file - * @throws JDOMException - * if an error occurs parsing the XML - * @throws IOException - * if an I/O error occurs reading the file - */ - @SuppressWarnings("null") - @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") - public JDom2XmlSchemaLoader(@NonNull Path path) throws JDOMException, IOException { - this(new SAXBuilder().build(path.toFile())); - } - - /** - * Constructs a new XML Schema loader from an input stream. - * - * @param is - * the input stream containing the XML Schema content - * @throws JDOMException - * if an error occurs parsing the XML - * @throws IOException - * if an I/O error occurs reading the stream - */ - @SuppressWarnings("null") - @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") - public JDom2XmlSchemaLoader(@NonNull InputStream is) throws JDOMException, IOException { - this(new SAXBuilder().build(is)); - } - - /** - * Constructs a new XML Schema loader from a JDOM2 document. - * - * @param document - * the JDOM2 document containing the XML Schema - */ - @SuppressFBWarnings("EI_EXPOSE_REP2") - public JDom2XmlSchemaLoader(@NonNull Document document) { - this.document = document; - } - - /** - * Retrieves the underlying JDOM2 document. - * - * @return the JDOM2 document - */ - protected Document getNode() { - return document; - } - - /** - * Executes an XPath query and returns matching elements. - * - * @param path - * the XPath expression to evaluate - * @param prefixToNamespaceMap - * a map of namespace prefixes to URIs for use in the XPath query - * @return a list of matching JDOM2 elements - */ - @SuppressWarnings("null") - @NonNull - public List getContent( - @NonNull String path, - @NonNull Map prefixToNamespaceMap) { - - Collection namespaces = prefixToNamespaceMap.entrySet().stream() - .map(entry -> Namespace.getNamespace(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - XPathExpression xpath = XPathFactory.instance().compile(path, Filters.element(), null, namespaces); - return xpath.evaluate(getNode()); - } -} diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlCoreDatatypeProvider.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlCoreDatatypeProvider.java index 87f1b33eb..3a90c4f92 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlCoreDatatypeProvider.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlCoreDatatypeProvider.java @@ -10,21 +10,28 @@ import gov.nist.secauto.metaschema.core.util.ObjectUtils; import org.eclipse.jdt.annotation.Owning; -import org.jdom2.Attribute; -import org.jdom2.Element; -import org.jdom2.filter.Filters; -import org.jdom2.xpath.XPathExpression; -import org.jdom2.xpath.XPathFactory; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; import java.io.InputStream; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + import edu.umd.cs.findbugs.annotations.NonNull; +/** + * Provides core XML Schema datatypes from the metaschema-datatypes.xsd + * resource. + */ public class XmlCoreDatatypeProvider extends AbstractXmlDatatypeProvider { @@ -36,20 +43,31 @@ protected InputStream getSchemaResource() { } @Override - protected List queryElements(JDom2XmlSchemaLoader loader) { + protected List queryElements(XmlSchemaLoader loader) { return loader.getContent( "/xs:schema/xs:simpleType", - CollectionUtil.singletonMap("xs", JDom2XmlSchemaLoader.NS_XML_SCHEMA)); + CollectionUtil.singletonMap("xs", XmlSchemaLoader.NS_XML_SCHEMA)); } @NonNull private static List analyzeDependencies(@NonNull Element element) { - XPathExpression xpath = XPathFactory.instance().compile(".//@base", Filters.attribute()); - return ObjectUtils.notNull(xpath.evaluate(element).stream() - .map(Attribute::getValue) - .filter(type -> !type.startsWith("xs:")) - .distinct() - .collect(Collectors.toList())); + try { + XPath xpath = XPathFactory.newInstance().newXPath(); + NodeList nodes = (NodeList) xpath.evaluate(".//@base", element, XPathConstants.NODESET); + + List dependencies = new ArrayList<>(); + for (int i = 0; i < nodes.getLength(); i++) { + String value = nodes.item(i).getNodeValue(); + if (value != null && !value.startsWith("xs:")) { + if (!dependencies.contains(value)) { + dependencies.add(value); + } + } + } + return dependencies; + } catch (XPathExpressionException ex) { + throw new IllegalStateException("Failed to analyze dependencies", ex); + } } @Override @@ -57,8 +75,8 @@ private static List analyzeDependencies(@NonNull Element element) { protected Map handleResults( @NonNull List items) { return ObjectUtils.notNull(items.stream() - .map(element -> new JDom2DatatypeContent( - ObjectUtils.requireNonNull(element.getAttributeValue("name")), + .map(element -> new DomDatatypeContent( + ObjectUtils.requireNonNull(element.getAttribute("name")), CollectionUtil.singletonList(element), analyzeDependencies(element))) .collect(Collectors.toMap((Function) IDatatypeContent::getTypeName, diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlProseBaseDatatypeProvider.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlProseBaseDatatypeProvider.java index 22e8ce7ca..fe1e5b685 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlProseBaseDatatypeProvider.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlProseBaseDatatypeProvider.java @@ -10,7 +10,7 @@ import gov.nist.secauto.metaschema.core.util.ObjectUtils; import org.eclipse.jdt.annotation.Owning; -import org.jdom2.Element; +import org.w3c.dom.Element; import java.io.InputStream; import java.util.List; @@ -18,6 +18,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; +/** + * Provides prose base datatype from the metaschema-prose-base.xsd resource. + */ public class XmlProseBaseDatatypeProvider extends AbstractXmlDatatypeProvider { private static final String DATATYPE_NAME = "ProseBase"; @@ -30,10 +33,10 @@ protected InputStream getSchemaResource() { } @Override - protected List queryElements(JDom2XmlSchemaLoader loader) { + protected List queryElements(XmlSchemaLoader loader) { return loader.getContent( "/xs:schema/*", - CollectionUtil.singletonMap("xs", JDom2XmlSchemaLoader.NS_XML_SCHEMA)); + CollectionUtil.singletonMap("xs", XmlSchemaLoader.NS_XML_SCHEMA)); } @Override @@ -41,7 +44,7 @@ protected List queryElements(JDom2XmlSchemaLoader loader) { protected Map handleResults(@NonNull List items) { return CollectionUtil.singletonMap( DATATYPE_NAME, - new JDom2DatatypeContent( + new DomDatatypeContent( DATATYPE_NAME, items, CollectionUtil.emptyList())); diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java new file mode 100644 index 000000000..0fdcede58 --- /dev/null +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Loads and queries XML Schema documents using standard Java DOM and XPath. + *

+ * This class provides functionality to load XML Schema documents from various + * sources and query their content using XPath expressions. It replaces the + * JDOM2-based implementation with standard Java XML APIs. + */ +public class XmlSchemaLoader { + /** The XML Schema namespace URI. */ + @NonNull + public static final String NS_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; + + @NonNull + private final Document document; + + /** + * Constructs a new XML Schema loader from a file path. + * + * @param path + * the path to the XML Schema file + * @throws SAXException + * if an error occurs parsing the XML + * @throws IOException + * if an I/O error occurs reading the file + */ + @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") + public XmlSchemaLoader(@NonNull Path path) throws SAXException, IOException { + this(parseDocument(path)); + } + + /** + * Constructs a new XML Schema loader from an input stream. + * + * @param is + * the input stream containing the XML Schema content + * @throws SAXException + * if an error occurs parsing the XML + * @throws IOException + * if an I/O error occurs reading the stream + */ + @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") + public XmlSchemaLoader(@NonNull InputStream is) throws SAXException, IOException { + this(parseDocument(is)); + } + + /** + * Constructs a new XML Schema loader from a DOM document. + * + * @param document + * the DOM document containing the XML Schema + */ + @SuppressFBWarnings("EI_EXPOSE_REP2") + public XmlSchemaLoader(@NonNull Document document) { + this.document = document; + } + + @NonNull + private static Document parseDocument(@NonNull Path path) throws SAXException, IOException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(path.toFile()); + // Normalize to ensure deferred DOM nodes are fully loaded for XPath evaluation + doc.normalizeDocument(); + return doc; + } catch (ParserConfigurationException ex) { + throw new IllegalStateException("Failed to create document builder", ex); + } + } + + @NonNull + private static Document parseDocument(@NonNull InputStream is) throws SAXException, IOException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(is); + // Normalize to ensure deferred DOM nodes are fully loaded for XPath evaluation + doc.normalizeDocument(); + return doc; + } catch (ParserConfigurationException ex) { + throw new IllegalStateException("Failed to create document builder", ex); + } + } + + /** + * Retrieves the underlying DOM document. + * + * @return the DOM document + */ + @SuppressFBWarnings("EI_EXPOSE_REP") + protected Document getDocument() { + return document; + } + + /** + * Executes an XPath query and returns matching elements. + * + * @param path + * the XPath expression to evaluate + * @param prefixToNamespaceMap + * a map of namespace prefixes to URIs for use in the XPath query + * @return a list of matching DOM elements + */ + @SuppressWarnings("null") + @NonNull + public List getContent( + @NonNull String path, + @NonNull Map prefixToNamespaceMap) { + + try { + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setNamespaceContext(new MapNamespaceContext(prefixToNamespaceMap)); + + NodeList nodeList = (NodeList) xpath.evaluate(path, document, XPathConstants.NODESET); + + List result = new ArrayList<>(nodeList.getLength()); + for (int i = 0; i < nodeList.getLength(); i++) { + if (nodeList.item(i) instanceof Element) { + result.add((Element) nodeList.item(i)); + } + } + return result; + } catch (XPathExpressionException ex) { + throw new IllegalArgumentException("Invalid XPath expression: " + path, ex); + } + } + + /** + * A simple NamespaceContext implementation backed by a Map. + */ + private static final class MapNamespaceContext implements NamespaceContext { + private final Map prefixToNamespace; + + MapNamespaceContext(Map prefixToNamespace) { + this.prefixToNamespace = prefixToNamespace; + } + + @Override + public String getNamespaceURI(String prefix) { + if (prefix == null) { + throw new IllegalArgumentException("prefix cannot be null"); + } + String uri = prefixToNamespace.get(prefix); + return uri != null ? uri : XMLConstants.NULL_NS_URI; + } + + @Override + public String getPrefix(String namespaceURI) { + if (namespaceURI == null) { + throw new IllegalArgumentException("namespaceURI cannot be null"); + } + for (Map.Entry entry : prefixToNamespace.entrySet()) { + if (namespaceURI.equals(entry.getValue())) { + return entry.getKey(); + } + } + return null; + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + List prefixes = new ArrayList<>(); + for (Map.Entry entry : prefixToNamespace.entrySet()) { + if (namespaceURI.equals(entry.getValue())) { + prefixes.add(entry.getKey()); + } + } + return prefixes.iterator(); + } + } +} diff --git a/schemagen/src/main/java/module-info.java b/schemagen/src/main/java/module-info.java index 8da332647..8d9abe023 100644 --- a/schemagen/src/main/java/module-info.java +++ b/schemagen/src/main/java/module-info.java @@ -19,9 +19,7 @@ requires nl.talsmasoftware.lazy4j; requires transitive org.apache.commons.lang3; requires org.apache.logging.log4j; - requires org.jdom2; - - requires Saxon.HE; + requires org.codehaus.stax2; requires org.eclipse.jdt.annotation; exports gov.nist.secauto.metaschema.schemagen; diff --git a/schemagen/src/main/resources/identity.xsl b/schemagen/src/main/resources/identity.xsl deleted file mode 100644 index f9783892a..000000000 --- a/schemagen/src/main/resources/identity.xsl +++ /dev/null @@ -1,26 +0,0 @@ - - - - - \ No newline at end of file diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/XmlSuiteTest.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/XmlSuiteTest.java index 50997b737..b6af63165 100644 --- a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/XmlSuiteTest.java +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/XmlSuiteTest.java @@ -18,14 +18,6 @@ import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader; import gov.nist.secauto.metaschema.schemagen.xml.XmlSchemaGenerator; -import org.jdom2.Document; -import org.jdom2.Element; -import org.jdom2.JDOMException; -import org.jdom2.Namespace; -import org.jdom2.filter.Filters; -import org.jdom2.input.StAXEventBuilder; -import org.jdom2.xpath.XPathExpression; -import org.jdom2.xpath.XPathFactory; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicNode; @@ -33,23 +25,31 @@ import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; import java.io.IOException; -import java.io.Reader; +import java.io.InputStream; import java.io.Writer; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; +import java.util.Iterator; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; -import javax.xml.stream.XMLEventReader; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLStreamException; +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; class XmlSuiteTest extends AbstractSchemaGeneratorTestSuite { @@ -152,7 +152,8 @@ void testLocalDeclarations() throws IOException, MetaschemaException { // NOPMD } @Test - void testLiboscalJavaIssue181() throws IOException, MetaschemaException, XMLStreamException, JDOMException { + void testLiboscalJavaIssue181() + throws IOException, MetaschemaException, SAXException, ParserConfigurationException, XPathExpressionException { IBindingContext bindingContext = newBindingContext(); IBindingModuleLoader loader = bindingContext.newModuleLoader(); @@ -173,20 +174,40 @@ void testLiboscalJavaIssue181() throws IOException, MetaschemaException, XMLStre } // check for missing attribute types per liboscal-java#181 - XMLInputFactory factory = XMLInputFactory.newFactory(); - try (Reader fileReader = Files.newBufferedReader(schemaPath, StandardCharsets.UTF_8)) { - XMLEventReader reader = factory.createXMLEventReader(fileReader); - StAXEventBuilder builder = new StAXEventBuilder(); - Document document = builder.build(reader); - - XPathExpression xpath = XPathFactory.instance() - .compile("//xs:attribute[not(@type or xs:simpleType)]", - Filters.element(), - null, - Namespace.getNamespace("xs", "http://www.w3.org/2001/XMLSchema")); - List result = xpath.evaluate(document); - - assertTrue(result.isEmpty()); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document; + try (InputStream is = Files.newInputStream(schemaPath)) { + document = builder.parse(is); } + + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setNamespaceContext(new NamespaceContext() { + @Override + public String getNamespaceURI(String prefix) { + if ("xs".equals(prefix)) { + return "http://www.w3.org/2001/XMLSchema"; + } + return null; + } + + @Override + public String getPrefix(String namespaceURI) { + return null; + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + return null; + } + }); + + NodeList result = (NodeList) xpath.evaluate( + "//xs:attribute[not(@type or xs:simpleType)]", + document, + XPathConstants.NODESET); + + assertTrue(result.getLength() == 0, "Found " + result.getLength() + " attributes without type"); } } diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContentTest.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContentTest.java new file mode 100644 index 000000000..fe33629db --- /dev/null +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/DomDatatypeContentTest.java @@ -0,0 +1,254 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import gov.nist.secauto.metaschema.core.util.CollectionUtil; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamWriter; + +/** + * Tests for DomDatatypeContent which writes DOM elements to XMLStreamWriter. + */ +class DomDatatypeContentTest { + + private static final String NS_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; + + private Element createSchemaElement(Document doc, String name) { + Element element = doc.createElementNS(NS_XML_SCHEMA, "xs:simpleType"); + element.setAttribute("name", name); + + Element restriction = doc.createElementNS(NS_XML_SCHEMA, "xs:restriction"); + restriction.setAttribute("base", "xs:string"); + element.appendChild(restriction); + + return element; + } + + private Document createDocument() throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.newDocument(); + } + + @Nested + @DisplayName("Basic Properties Tests") + class BasicPropertiesTests { + + @Test + @DisplayName("getTypeName returns correct type name") + void testGetTypeName() throws Exception { + Document doc = createDocument(); + Element element = createSchemaElement(doc, "TestDatatype"); + + DomDatatypeContent content = new DomDatatypeContent( + "TestDatatype", + CollectionUtil.singletonList(element), + CollectionUtil.emptyList()); + + assertEquals("TestDatatype", content.getTypeName()); + } + + @Test + @DisplayName("getDependencies returns correct dependencies") + void testGetDependencies() throws Exception { + Document doc = createDocument(); + Element element = createSchemaElement(doc, "TestDatatype"); + List dependencies = Arrays.asList("BaseDatatypeA", "BaseDatatypeB"); + + DomDatatypeContent content = new DomDatatypeContent( + "TestDatatype", + CollectionUtil.singletonList(element), + dependencies); + + assertEquals(2, content.getDependencies().size()); + assertTrue(content.getDependencies().contains("BaseDatatypeA")); + assertTrue(content.getDependencies().contains("BaseDatatypeB")); + } + + @Test + @DisplayName("empty dependencies list works correctly") + void testEmptyDependencies() throws Exception { + Document doc = createDocument(); + Element element = createSchemaElement(doc, "TestDatatype"); + + DomDatatypeContent content = new DomDatatypeContent( + "TestDatatype", + CollectionUtil.singletonList(element), + CollectionUtil.emptyList()); + + assertNotNull(content.getDependencies()); + assertTrue(content.getDependencies().isEmpty()); + } + } + + @Nested + @DisplayName("Write to XMLStreamWriter Tests") + class WriteTests { + + @Test + @DisplayName("writes simple element correctly") + void testWriteSimpleElement() throws Exception { + Document doc = createDocument(); + Element element = createSchemaElement(doc, "StringDatatype"); + + DomDatatypeContent content = new DomDatatypeContent( + "StringDatatype", + CollectionUtil.singletonList(element), + CollectionUtil.emptyList()); + + StringWriter sw = new StringWriter(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = factory.createXMLStreamWriter(sw); + + writer.writeStartDocument(); + writer.writeStartElement("wrapper"); + content.write(writer); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + String result = sw.toString(); + + // Verify the element was written + assertTrue(result.contains("simpleType"), "Should contain simpleType element"); + assertTrue(result.contains("StringDatatype"), "Should contain type name attribute"); + assertTrue(result.contains("restriction"), "Should contain child elements"); + } + + @Test + @DisplayName("writes multiple elements correctly") + void testWriteMultipleElements() throws Exception { + Document doc = createDocument(); + Element element1 = createSchemaElement(doc, "Type1"); + Element element2 = createSchemaElement(doc, "Type2"); + + DomDatatypeContent content = new DomDatatypeContent( + "MultiType", + Arrays.asList(element1, element2), + CollectionUtil.emptyList()); + + StringWriter sw = new StringWriter(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = factory.createXMLStreamWriter(sw); + + writer.writeStartDocument(); + writer.writeStartElement("wrapper"); + content.write(writer); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + String result = sw.toString(); + + // Verify both elements were written + assertTrue(result.contains("Type1"), "Should contain first type"); + assertTrue(result.contains("Type2"), "Should contain second type"); + } + + @Test + @DisplayName("preserves element namespace") + void testPreservesNamespace() throws Exception { + Document doc = createDocument(); + Element element = createSchemaElement(doc, "TestType"); + + DomDatatypeContent content = new DomDatatypeContent( + "TestType", + CollectionUtil.singletonList(element), + CollectionUtil.emptyList()); + + StringWriter sw = new StringWriter(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = factory.createXMLStreamWriter(sw); + + writer.writeStartDocument(); + writer.writeStartElement("wrapper"); + content.write(writer); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + String result = sw.toString(); + + // The output should preserve the xs prefix or include the namespace + assertTrue(result.contains("simpleType"), "Should contain the element"); + } + + @Test + @DisplayName("handles element with text content") + void testElementWithTextContent() throws Exception { + Document doc = createDocument(); + Element element = doc.createElementNS(NS_XML_SCHEMA, "xs:annotation"); + Element docElement = doc.createElementNS(NS_XML_SCHEMA, "xs:documentation"); + docElement.setTextContent("This is documentation text"); + element.appendChild(docElement); + + DomDatatypeContent content = new DomDatatypeContent( + "AnnotatedType", + CollectionUtil.singletonList(element), + CollectionUtil.emptyList()); + + StringWriter sw = new StringWriter(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = factory.createXMLStreamWriter(sw); + + writer.writeStartDocument(); + writer.writeStartElement("wrapper"); + content.write(writer); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + String result = sw.toString(); + + assertTrue(result.contains("documentation"), "Should contain documentation element"); + assertTrue(result.contains("This is documentation text"), "Should contain text content"); + } + + @Test + @DisplayName("handles empty element list") + void testEmptyElementList() throws Exception { + DomDatatypeContent content = new DomDatatypeContent( + "EmptyType", + CollectionUtil.emptyList(), + CollectionUtil.emptyList()); + + StringWriter sw = new StringWriter(); + XMLOutputFactory factory = XMLOutputFactory.newInstance(); + XMLStreamWriter writer = factory.createXMLStreamWriter(sw); + + writer.writeStartDocument(); + writer.writeStartElement("wrapper"); + content.write(writer); + writer.writeEndElement(); + writer.writeEndDocument(); + writer.close(); + + String result = sw.toString(); + + // Should just have wrapper with no content + assertFalse(result.contains("simpleType"), "Should not contain any type elements"); + } + } +} diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java new file mode 100644 index 000000000..bf93072fc --- /dev/null +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java @@ -0,0 +1,565 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +/** + * Tests for {@link IndentingXMLStreamWriter}. + *

+ * These tests verify that the indenting wrapper correctly formats XML output + * while preserving text content exactly (no spurious whitespace in mixed + * content). + */ +class IndentingXMLStreamWriterTest { + + private static final String NEWLINE = "\n"; + private static final String INDENT = " "; + + /** + * Helper to create an IndentingXMLStreamWriter wrapping a StringWriter. + */ + private static IndentingXMLStreamWriter createWriter(StringWriter stringWriter) throws XMLStreamException { + XMLOutputFactory factory = XMLOutputFactory.newFactory(); + XMLStreamWriter delegate = factory.createXMLStreamWriter(stringWriter); + return new IndentingXMLStreamWriter(delegate); + } + + @Nested + @DisplayName("Element Structure Tests") + class ElementStructureTests { + + @Test + @DisplayName("single element is indented") + void testSingleElementIndentation() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + // Note: StAX implementations may use single or double quotes in XML declaration + String expected = "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("nested elements are indented at each level") + void testNestedElementIndentation() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("child"); + writer.writeStartElement("grandchild"); + writer.writeEndElement(); // grandchild + writer.writeEndElement(); // child + writer.writeEndElement(); // root + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + INDENT + "" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("sibling elements at same level") + void testSiblingElements() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("child1"); + writer.writeEndElement(); + writer.writeStartElement("child2"); + writer.writeEndElement(); + writer.writeStartElement("child3"); + writer.writeEndElement(); + writer.writeEndElement(); // root + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("empty element with attributes") + void testEmptyElementWithAttributes() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("element"); + writer.writeAttribute("name", "value"); + writer.writeAttribute("other", "data"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("namespace declarations") + void testNamespaceDeclarations() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("xs", "schema", "http://www.w3.org/2001/XMLSchema"); + writer.writeNamespace("xs", "http://www.w3.org/2001/XMLSchema"); + writer.writeStartElement("xs", "element", "http://www.w3.org/2001/XMLSchema"); + writer.writeAttribute("name", "test"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + // Just verify it doesn't throw and produces valid output + String result = sw.toString(); + assertEquals(true, result.contains("" + NEWLINE + + INDENT + "Some text content" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("mixed content preserves text exactly") + void testMixedContentPreservesText() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("p"); + writer.writeCharacters("Some text with "); + writer.writeStartElement("b"); + writer.writeCharacters("bold"); + writer.writeEndElement(); // b + writer.writeCharacters(" words"); + writer.writeEndElement(); // p + writer.writeEndDocument(); + } + + // Mixed content must NOT have any added whitespace + String expected = "" + NEWLINE + + "

Some text with bold words

"; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("inline elements within text do not gain whitespace") + void testInlineElementsNoWhitespace() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("p"); + writer.writeCharacters("Click "); + writer.writeStartElement("a"); + writer.writeAttribute("href", "http://example.com"); + writer.writeCharacters("here"); + writer.writeEndElement(); // a + writer.writeCharacters(" for more info."); + writer.writeEndElement(); // p + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "

Click here for more info.

"; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("text with leading/trailing whitespace is preserved") + void testTextWhitespacePreserved() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("element"); + writer.writeCharacters(" leading and trailing spaces "); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + " leading and trailing spaces " + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("whitespace-only text triggers mixed content mode") + void testWhitespaceOnlyTextPreserved() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("element"); + writer.writeCharacters(" "); + writer.writeStartElement("child"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + // Once whitespace text is written, we're in mixed content mode + // The child element should NOT get indentation added + String result = sw.toString(); + assertEquals(true, result.contains(" ")); + } + + @Test + @DisplayName("nested mixed content elements") + void testNestedMixedContent() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("doc"); + writer.writeCharacters("Text "); + writer.writeStartElement("em"); + writer.writeCharacters("with "); + writer.writeStartElement("strong"); + writer.writeCharacters("nested"); + writer.writeEndElement(); // strong + writer.writeCharacters(" emphasis"); + writer.writeEndElement(); // em + writer.writeCharacters(" content."); + writer.writeEndElement(); // doc + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "Text with nested emphasis content."; + assertEquals(expected, sw.toString()); + } + } + + @Nested + @DisplayName("Special Content Tests") + class SpecialContentTests { + + @Test + @DisplayName("CDATA sections are not indented internally") + void testCDataNotIndented() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("element"); + writer.writeCData("CDATA content with chars"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String result = sw.toString(); + assertEquals(true, result.contains(" chars]]>")); + } + + @Test + @DisplayName("comments are properly indented") + void testCommentsIndented() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeComment("This is a comment"); + writer.writeStartElement("child"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("processing instructions are properly indented") + void testProcessingInstructionsIndented() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeProcessingInstruction("target", "data"); + writer.writeStartElement("child"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("comments in mixed content do not add whitespace") + void testCommentsInMixedContent() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("p"); + writer.writeCharacters("Before "); + writer.writeComment("inline comment"); + writer.writeCharacters(" after"); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "

Before after

"; + assertEquals(expected, sw.toString()); + } + } + + @Nested + @DisplayName("Schema Documentation Tests (xs:documentation with XHTML)") + class SchemaDocumentationTests { + + @Test + @DisplayName("xs:documentation with xhtml:p elements") + void testDocumentationWithParagraphs() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("xs", "schema", "http://www.w3.org/2001/XMLSchema"); + writer.writeStartElement("xs", "annotation", "http://www.w3.org/2001/XMLSchema"); + writer.writeStartElement("xs", "documentation", "http://www.w3.org/2001/XMLSchema"); + writer.writeStartElement("p", "p", "http://www.w3.org/1999/xhtml"); + writer.writeCharacters("This is documentation."); + writer.writeEndElement(); // p + writer.writeEndElement(); // documentation + writer.writeEndElement(); // annotation + writer.writeEndElement(); // schema + writer.writeEndDocument(); + } + + String result = sw.toString(); + // The p element should be indented as element-only content + // But the text inside p should not have added whitespace + assertEquals(true, result.contains("This is documentation.")); + } + + @Test + @DisplayName("xs:documentation with inline xhtml:b elements") + void testDocumentationWithBoldInline() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("xs", "documentation", "http://www.w3.org/2001/XMLSchema"); + writer.writeStartElement("p", "p", "http://www.w3.org/1999/xhtml"); + writer.writeCharacters("Text with "); + writer.writeStartElement("b", "b", "http://www.w3.org/1999/xhtml"); + writer.writeCharacters("bold"); + writer.writeEndElement(); // b + writer.writeCharacters(" content."); + writer.writeEndElement(); // p + writer.writeEndElement(); // documentation + writer.writeEndDocument(); + } + + String result = sw.toString(); + // No whitespace corruption in mixed content + assertEquals(true, result.contains("Text with bold content.")); + } + + @Test + @DisplayName("nested XHTML with paragraphs containing bold/italic") + void testNestedXhtml() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("doc"); + writer.writeStartElement("p"); + writer.writeCharacters("A paragraph with "); + writer.writeStartElement("b"); + writer.writeCharacters("bold and "); + writer.writeStartElement("i"); + writer.writeCharacters("italic"); + writer.writeEndElement(); // i + writer.writeEndElement(); // b + writer.writeCharacters(" text."); + writer.writeEndElement(); // p + writer.writeEndElement(); // doc + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "

A paragraph with bold and italic text.

" + NEWLINE + + "
"; + assertEquals(expected, sw.toString()); + } + } + + @Nested + @DisplayName("Edge Cases") + class EdgeCaseTests { + + @Test + @DisplayName("deeply nested elements") + void testDeeplyNestedElements() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("l1"); + writer.writeStartElement("l2"); + writer.writeStartElement("l3"); + writer.writeStartElement("l4"); + writer.writeStartElement("l5"); + writer.writeEndElement(); // l5 + writer.writeEndElement(); // l4 + writer.writeEndElement(); // l3 + writer.writeEndElement(); // l2 + writer.writeEndElement(); // l1 + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + INDENT + "" + NEWLINE + + INDENT + INDENT + INDENT + "" + NEWLINE + + INDENT + INDENT + INDENT + INDENT + "" + NEWLINE + + INDENT + INDENT + INDENT + "" + NEWLINE + + INDENT + INDENT + "" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("mixed siblings: some with text, some without") + void testMixedSiblings() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("empty"); + writer.writeEndElement(); + writer.writeStartElement("withText"); + writer.writeCharacters("content"); + writer.writeEndElement(); + writer.writeStartElement("alsoEmpty"); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + String expected = "" + NEWLINE + + "" + NEWLINE + + INDENT + "" + NEWLINE + + INDENT + "content" + NEWLINE + + INDENT + "" + NEWLINE + + ""; + assertEquals(expected, sw.toString()); + } + + @Test + @DisplayName("text after child element in parent") + void testTextAfterChildElement() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeStartElement("child"); + writer.writeCharacters("child text"); + writer.writeEndElement(); + writer.writeCharacters("parent text after child"); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + // Once text is written to parent, subsequent content should not be indented + String result = sw.toString(); + // The child was indented, but after the text, parent is in mixed content mode + assertEquals(true, result.contains("parent text after child")); + } + + @Test + @DisplayName("element after text in same parent") + void testElementAfterTextInSameParent() throws XMLStreamException { + StringWriter sw = new StringWriter(); + try (IndentingXMLStreamWriter writer = createWriter(sw)) { + writer.writeStartDocument("UTF-8", "1.0"); + writer.writeStartElement("root"); + writer.writeCharacters("text first "); + writer.writeStartElement("child"); + writer.writeEndElement(); + writer.writeCharacters(" text last"); + writer.writeEndElement(); + writer.writeEndDocument(); + } + + // Parent has text, so child should not be indented + String expected = "" + NEWLINE + + "text first text last"; + assertEquals(expected, sw.toString()); + } + } +} diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java new file mode 100644 index 000000000..8efa4d7c4 --- /dev/null +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java @@ -0,0 +1,185 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.schemagen.xml.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import gov.nist.secauto.metaschema.core.model.IModule; +import gov.nist.secauto.metaschema.core.util.CollectionUtil; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Tests for the standard DOM/XPath-based XML schema loading functionality. + *

+ * These tests verify that the new DOM-based implementation provides the same + * functionality as the previous JDOM2-based implementation. + */ +class XmlSchemaLoaderTest { + + private static final String NS_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; + private static final Map XS_NAMESPACE_MAP = CollectionUtil.singletonMap("xs", NS_XML_SCHEMA); + + private static XmlSchemaLoader datatypesLoader; + private static XmlSchemaLoader proseBaseLoader; + + @BeforeAll + static void loadSchemas() throws SAXException, IOException { + try (InputStream is = IModule.class.getResourceAsStream("/schema/xml/metaschema-datatypes.xsd")) { + assertNotNull(is, "metaschema-datatypes.xsd should be on classpath"); + datatypesLoader = new XmlSchemaLoader(is); + } + + try (InputStream is = IModule.class.getResourceAsStream("/schema/xml/metaschema-prose-base.xsd")) { + assertNotNull(is, "metaschema-prose-base.xsd should be on classpath"); + proseBaseLoader = new XmlSchemaLoader(is); + } + } + + @Nested + @DisplayName("XPath Query Tests") + class XPathQueryTests { + + @Test + @DisplayName("/xs:schema/xs:simpleType returns expected elements from datatypes.xsd") + void testSimpleTypeQuery() { + List elements = datatypesLoader.getContent("/xs:schema/xs:simpleType", XS_NAMESPACE_MAP); + + assertNotNull(elements); + assertFalse(elements.isEmpty(), "Should find simpleType elements"); + + // Verify each element has a name attribute + for (Element element : elements) { + assertNotNull(element.getAttribute("name"), + "Each simpleType should have a name attribute"); + assertFalse(element.getAttribute("name").isEmpty(), + "Name attribute should not be empty"); + } + + // Verify we found expected datatypes + List typeNames = elements.stream() + .map(e -> e.getAttribute("name")) + .collect(Collectors.toList()); + + // Check for some expected core datatypes + assertEquals(true, typeNames.contains("Base64Datatype"), + "Should contain Base64Datatype"); + assertEquals(true, typeNames.contains("StringDatatype"), + "Should contain StringDatatype"); + } + + @Test + @DisplayName("/xs:schema/xs:simpleType returns expected elements from prose-base.xsd") + void testSimpleTypeQueryProseBase() { + List elements = proseBaseLoader.getContent("/xs:schema/xs:simpleType", XS_NAMESPACE_MAP); + + assertNotNull(elements); + // prose-base.xsd may have fewer or different simpleTypes + // Just verify the query works and returns elements + } + + @Test + @DisplayName("/xs:schema/* returns all child elements") + void testAllChildrenQuery() { + List elements = datatypesLoader.getContent("/xs:schema/*", XS_NAMESPACE_MAP); + + assertNotNull(elements); + assertFalse(elements.isEmpty(), "Should find child elements"); + + // Should include various element types (simpleType, annotation, etc.) + List elementNames = elements.stream() + .map(Element::getLocalName) + .distinct() + .collect(Collectors.toList()); + + assertEquals(true, elementNames.contains("simpleType"), + "Should contain simpleType elements"); + } + } + + @Nested + @DisplayName("Element Content Tests") + class ElementContentTests { + + @Test + @DisplayName("elements have correct namespace") + void testElementNamespace() { + List elements = datatypesLoader.getContent("/xs:schema/xs:simpleType", XS_NAMESPACE_MAP); + + assertFalse(elements.isEmpty()); + + Element first = elements.get(0); + assertEquals(NS_XML_SCHEMA, first.getNamespaceURI(), + "Element should have XML Schema namespace"); + assertEquals("simpleType", first.getLocalName(), + "Element should be named simpleType"); + } + + @Test + @DisplayName("elements contain expected child structure") + void testElementChildStructure() { + List elements = datatypesLoader.getContent("/xs:schema/xs:simpleType", XS_NAMESPACE_MAP); + + // Find a specific datatype to test structure + Element stringType = elements.stream() + .filter(e -> "StringDatatype".equals(e.getAttribute("name"))) + .findFirst() + .orElse(null); + + assertNotNull(stringType, "Should find StringDatatype"); + + // StringDatatype should have restriction or other content + // Count child elements (DOM uses NodeList, not getChildren) + int childElementCount = 0; + for (int i = 0; i < stringType.getChildNodes().getLength(); i++) { + if (stringType.getChildNodes().item(i) instanceof Element) { + childElementCount++; + } + } + assertFalse(childElementCount == 0, "StringDatatype should have child elements"); + } + } + + @Nested + @DisplayName("Loading Tests") + class LoadingTests { + + @Test + @DisplayName("can load schema from InputStream") + void testLoadFromInputStream() throws SAXException, IOException { + try (InputStream is = IModule.class.getResourceAsStream("/schema/xml/metaschema-datatypes.xsd")) { + assertNotNull(is); + XmlSchemaLoader loader = new XmlSchemaLoader(is); + + List elements = loader.getContent("/xs:schema/xs:simpleType", XS_NAMESPACE_MAP); + assertFalse(elements.isEmpty()); + } + } + + @Test + @DisplayName("handles empty result gracefully") + void testEmptyResult() { + // Query for non-existent elements + List elements = datatypesLoader.getContent("/xs:schema/xs:nonExistent", XS_NAMESPACE_MAP); + + assertNotNull(elements, "Should return empty list, not null"); + assertEquals(0, elements.size(), "Should return empty list for non-matching query"); + } + } +} From 67b7ca04c26a365ad8c00b7450668a006b93e009 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 11:28:11 -0500 Subject: [PATCH 3/7] chore: remove unused dependency declarations from parent POM Remove version properties and dependencyManagement entries for: - org.jdom:jdom2 - jaxen:jaxen - net.sf.saxon:Saxon-HE - org.xmlresolver:xmlresolver These dependencies are no longer used by any module. --- pom.xml | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/pom.xml b/pom.xml index eb486b697..dafd1cbbe 100644 --- a/pom.xml +++ b/pom.xml @@ -53,8 +53,6 @@ 2.20.1 2.4.2 4.0.0 - 2.0.0 - 2.0.6.1 2.13.1 20251224 2.0.3 @@ -65,12 +63,10 @@ ${project.parent.version} 4.0.2 1.2.0 - 12.9 4.2.2 3.1.3.RELEASE 7.1.1 3.0.32 - 6.0.19 2.0.2 @@ -221,17 +217,6 @@ cli-processor ${project.version} - - org.jdom - jdom2 - ${dependency.jdom2.version} - - - - jaxen - jaxen - ${dependency.jaxen.version} - org.eclipse.persistence org.eclipse.persistence.moxy @@ -242,35 +227,6 @@ stax2-api ${dependency.stax2-api.version} - - net.sf.saxon - Saxon-HE - ${dependency.saxon.version} - - - org.xmlresolver - xmlresolver - ${dependency.xmlresolver.version} - - - xml-apis - xml-apis - - - - - - org.xmlresolver - xmlresolver - data - ${dependency.xmlresolver.version} - - - xml-apis - xml-apis - - - xml-apis xml-apis From 1e9b28a8cf067d81440ea9466eef2888f23eb1cf Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 12:02:16 -0500 Subject: [PATCH 4/7] docs: add Javadoc to IndentingXMLStreamWriter2 override methods Add {@inheritDoc} Javadoc comments to all override methods in IndentingXMLStreamWriter2 to address CodeRabbit review feedback. --- .../xml/impl/IndentingXMLStreamWriter2.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java index da53a86ee..1c83bdf15 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriter2.java @@ -57,77 +57,122 @@ protected XMLStreamWriter2 getDelegate2() { // XMLStreamWriter2-specific methods - delegate to underlying writer // ============================================================ + /** + * {@inheritDoc} + */ @Override public boolean isPropertySupported(String name) { return delegate2.isPropertySupported(name); } + /** + * {@inheritDoc} + */ @Override public boolean setProperty(String name, Object value) { return delegate2.setProperty(name, value); } + /** + * {@inheritDoc} + */ @Override public XMLStreamLocation2 getLocation() { return delegate2.getLocation(); } + /** + * {@inheritDoc} + */ @Override public String getEncoding() { return delegate2.getEncoding(); } + /** + * {@inheritDoc} + */ @Override public void writeSpace(String text) throws XMLStreamException { delegate2.writeSpace(text); } + /** + * {@inheritDoc} + */ @Override public void writeSpace(char[] text, int offset, int length) throws XMLStreamException { delegate2.writeSpace(text, offset, length); } + /** + * {@inheritDoc} + */ @Override public void writeRaw(String text) throws XMLStreamException { delegate2.writeRaw(text); } + /** + * {@inheritDoc} + */ @Override public void writeRaw(String text, int offset, int length) throws XMLStreamException { delegate2.writeRaw(text, offset, length); } + /** + * {@inheritDoc} + */ @Override public void writeRaw(char[] text, int offset, int length) throws XMLStreamException { delegate2.writeRaw(text, offset, length); } + /** + * {@inheritDoc} + */ @Override public void copyEventFromReader(XMLStreamReader2 reader, boolean preserveEventData) throws XMLStreamException { delegate2.copyEventFromReader(reader, preserveEventData); } + /** + * {@inheritDoc} + */ @Override public void closeCompletely() throws XMLStreamException { delegate2.closeCompletely(); } + /** + * {@inheritDoc} + */ @Override public void writeDTD(String rootName, String systemId, String publicId, String internalSubset) throws XMLStreamException { delegate2.writeDTD(rootName, systemId, publicId, internalSubset); } + /** + * {@inheritDoc} + */ @Override public void writeFullEndElement() throws XMLStreamException { delegate2.writeFullEndElement(); } + /** + * {@inheritDoc} + */ @Override public void writeStartDocument(String version, String encoding, boolean standAlone) throws XMLStreamException { delegate2.writeStartDocument(version, encoding, standAlone); } + /** + * {@inheritDoc} + */ @Override public void writeCData(char[] text, int start, int len) throws XMLStreamException { delegate2.writeCData(text, start, len); @@ -137,155 +182,239 @@ public void writeCData(char[] text, int start, int len) throws XMLStreamExceptio // TypedXMLStreamWriter methods - delegate to underlying writer // ============================================================ + /** + * {@inheritDoc} + */ @Override public void writeBoolean(boolean value) throws XMLStreamException { delegate2.writeBoolean(value); } + /** + * {@inheritDoc} + */ @Override public void writeInt(int value) throws XMLStreamException { delegate2.writeInt(value); } + /** + * {@inheritDoc} + */ @Override public void writeLong(long value) throws XMLStreamException { delegate2.writeLong(value); } + /** + * {@inheritDoc} + */ @Override public void writeFloat(float value) throws XMLStreamException { delegate2.writeFloat(value); } + /** + * {@inheritDoc} + */ @Override public void writeDouble(double value) throws XMLStreamException { delegate2.writeDouble(value); } + /** + * {@inheritDoc} + */ @Override public void writeInteger(BigInteger value) throws XMLStreamException { delegate2.writeInteger(value); } + /** + * {@inheritDoc} + */ @Override public void writeDecimal(BigDecimal value) throws XMLStreamException { delegate2.writeDecimal(value); } + /** + * {@inheritDoc} + */ @Override public void writeQName(javax.xml.namespace.QName name) throws XMLStreamException { delegate2.writeQName(name); } + /** + * {@inheritDoc} + */ @Override public void writeBinary(byte[] value, int from, int length) throws XMLStreamException { delegate2.writeBinary(value, from, length); } + /** + * {@inheritDoc} + */ @Override public void writeBinary(org.codehaus.stax2.typed.Base64Variant variant, byte[] value, int from, int length) throws XMLStreamException { delegate2.writeBinary(variant, value, from, length); } + /** + * {@inheritDoc} + */ @Override public void writeIntArray(int[] value, int from, int length) throws XMLStreamException { delegate2.writeIntArray(value, from, length); } + /** + * {@inheritDoc} + */ @Override public void writeLongArray(long[] value, int from, int length) throws XMLStreamException { delegate2.writeLongArray(value, from, length); } + /** + * {@inheritDoc} + */ @Override public void writeFloatArray(float[] value, int from, int length) throws XMLStreamException { delegate2.writeFloatArray(value, from, length); } + /** + * {@inheritDoc} + */ @Override public void writeDoubleArray(double[] value, int from, int length) throws XMLStreamException { delegate2.writeDoubleArray(value, from, length); } + /** + * {@inheritDoc} + */ @Override public void writeBooleanAttribute(String prefix, String namespaceURI, String localName, boolean value) throws XMLStreamException { delegate2.writeBooleanAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeIntAttribute(String prefix, String namespaceURI, String localName, int value) throws XMLStreamException { delegate2.writeIntAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeLongAttribute(String prefix, String namespaceURI, String localName, long value) throws XMLStreamException { delegate2.writeLongAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeFloatAttribute(String prefix, String namespaceURI, String localName, float value) throws XMLStreamException { delegate2.writeFloatAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeDoubleAttribute(String prefix, String namespaceURI, String localName, double value) throws XMLStreamException { delegate2.writeDoubleAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeIntegerAttribute(String prefix, String namespaceURI, String localName, BigInteger value) throws XMLStreamException { delegate2.writeIntegerAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeDecimalAttribute(String prefix, String namespaceURI, String localName, BigDecimal value) throws XMLStreamException { delegate2.writeDecimalAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeQNameAttribute(String prefix, String namespaceURI, String localName, javax.xml.namespace.QName name) throws XMLStreamException { delegate2.writeQNameAttribute(prefix, namespaceURI, localName, name); } + /** + * {@inheritDoc} + */ @Override public void writeBinaryAttribute(String prefix, String namespaceURI, String localName, byte[] value) throws XMLStreamException { delegate2.writeBinaryAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeBinaryAttribute(org.codehaus.stax2.typed.Base64Variant variant, String prefix, String namespaceURI, String localName, byte[] value) throws XMLStreamException { delegate2.writeBinaryAttribute(variant, prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeIntArrayAttribute(String prefix, String namespaceURI, String localName, int[] value) throws XMLStreamException { delegate2.writeIntArrayAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeLongArrayAttribute(String prefix, String namespaceURI, String localName, long[] value) throws XMLStreamException { delegate2.writeLongArrayAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeFloatArrayAttribute(String prefix, String namespaceURI, String localName, float[] value) throws XMLStreamException { delegate2.writeFloatArrayAttribute(prefix, namespaceURI, localName, value); } + /** + * {@inheritDoc} + */ @Override public void writeDoubleArrayAttribute(String prefix, String namespaceURI, String localName, double[] value) throws XMLStreamException { @@ -296,21 +425,33 @@ public void writeDoubleArrayAttribute(String prefix, String namespaceURI, String // Validatable methods - delegate to underlying writer // ============================================================ + /** + * {@inheritDoc} + */ @Override public XMLValidator validateAgainst(XMLValidationSchema schema) throws XMLStreamException { return delegate2.validateAgainst(schema); } + /** + * {@inheritDoc} + */ @Override public XMLValidator stopValidatingAgainst(XMLValidationSchema schema) throws XMLStreamException { return delegate2.stopValidatingAgainst(schema); } + /** + * {@inheritDoc} + */ @Override public XMLValidator stopValidatingAgainst(XMLValidator validator) throws XMLStreamException { return delegate2.stopValidatingAgainst(validator); } + /** + * {@inheritDoc} + */ @Override public ValidationProblemHandler setValidationProblemHandler(ValidationProblemHandler handler) { return delegate2.setValidationProblemHandler(handler); From ad9bbc0423e9a9424370f6cd73d56e392a6a55a6 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 13:42:09 -0500 Subject: [PATCH 5/7] refactor: explicitly instantiate WstxOutputFactory for StAX provider Replace XMLOutputFactory.newInstance() with direct WstxOutputFactory instantiation to ensure Woodstox is always used, regardless of assertions being enabled. Also add rule about not excusing test failures as "pre-existing". --- .claude/rules/unit-testing.md | 13 +++++++++++++ .../schemagen/xml/XmlSchemaGenerator.java | 3 +-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.claude/rules/unit-testing.md b/.claude/rules/unit-testing.md index f0e14a474..e1d8129c5 100644 --- a/.claude/rules/unit-testing.md +++ b/.claude/rules/unit-testing.md @@ -15,6 +15,19 @@ 3. Re-run CI to verify it's not caused by your changes 4. Open an issue to track the flaky test if not already tracked +### No Excuses for Test Failures (BLOCKING) + +**"Pre-existing failure" is NOT a valid excuse.** Any broken test in your branch IS your responsibility: + +- Do not claim "tests were already failing before my changes" +- Do not dismiss failures as "not caused by my change" +- Do not proceed with commits or pushes when tests fail + +**When encountering test failures:** +1. Fix them, even if they predate your changes +2. If truly unrelated, stash your work, fix on a separate branch, and merge +3. The 100% pass rate policy has no exceptions + ## Core Principles ### What NOT to Test diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java index 1178c9aa6..2a5eb1616 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/XmlSchemaGenerator.java @@ -76,8 +76,7 @@ public class XmlSchemaGenerator */ @NonNull private static XMLOutputFactory2 defaultXMLOutputFactory() { - XMLOutputFactory2 xmlOutputFactory = (XMLOutputFactory2) XMLOutputFactory.newInstance(); - assert xmlOutputFactory instanceof WstxOutputFactory; + WstxOutputFactory xmlOutputFactory = new WstxOutputFactory(); xmlOutputFactory.configureForSpeed(); xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true); return xmlOutputFactory; From f76475a77918c667748284e2734767a5aaaa2b71 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 14:02:00 -0500 Subject: [PATCH 6/7] refactor: address CodeRabbit nitpicks - Replace assertEquals(true, ...) with assertTrue() in tests - Extract common createDocumentBuilder() in XmlSchemaLoader --- .../schemagen/xml/impl/XmlSchemaLoader.java | 36 ++++++++++--------- .../AbstractSchemaGeneratorTestSuite.java | 3 +- .../impl/IndentingXMLStreamWriterTest.java | 15 ++++---- .../xml/impl/XmlSchemaLoaderTest.java | 7 ++-- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java index 0fdcede58..5451f2997 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java @@ -87,34 +87,36 @@ public XmlSchemaLoader(@NonNull Document document) { this.document = document; } + /** + * Creates a namespace-aware document builder. + * + * @return a configured document builder + */ @NonNull - private static Document parseDocument(@NonNull Path path) throws SAXException, IOException { + private static DocumentBuilder createDocumentBuilder() { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(path.toFile()); - // Normalize to ensure deferred DOM nodes are fully loaded for XPath evaluation - doc.normalizeDocument(); - return doc; + return factory.newDocumentBuilder(); } catch (ParserConfigurationException ex) { throw new IllegalStateException("Failed to create document builder", ex); } } + @NonNull + private static Document parseDocument(@NonNull Path path) throws SAXException, IOException { + Document doc = createDocumentBuilder().parse(path.toFile()); + // Normalize to ensure deferred DOM nodes are fully loaded for XPath evaluation + doc.normalizeDocument(); + return doc; + } + @NonNull private static Document parseDocument(@NonNull InputStream is) throws SAXException, IOException { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(is); - // Normalize to ensure deferred DOM nodes are fully loaded for XPath evaluation - doc.normalizeDocument(); - return doc; - } catch (ParserConfigurationException ex) { - throw new IllegalStateException("Failed to create document builder", ex); - } + Document doc = createDocumentBuilder().parse(is); + // Normalize to ensure deferred DOM nodes are fully loaded for XPath evaluation + doc.normalizeDocument(); + return doc; } /** diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/AbstractSchemaGeneratorTestSuite.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/AbstractSchemaGeneratorTestSuite.java index 25aa8a618..ae6c92480 100644 --- a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/AbstractSchemaGeneratorTestSuite.java +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/AbstractSchemaGeneratorTestSuite.java @@ -6,6 +6,7 @@ package gov.nist.secauto.metaschema.schemagen; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration; import gov.nist.secauto.metaschema.core.configuration.IConfiguration; @@ -265,7 +266,7 @@ protected void doTest( case JSON: case YAML: Path jsonSchema = produceJsonSchema(module, generationDir.resolve(generatedSchemaName + ".json")); - assertEquals(true, validateWithSchema(JSON_SCHEMA_VALIDATOR, jsonSchema), + assertTrue(validateWithSchema(JSON_SCHEMA_VALIDATOR, jsonSchema), String.format("JSON schema '%s' was invalid", jsonSchema.toString())); schemaPath = jsonSchema; break; diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java index bf93072fc..5b4b79bb4 100644 --- a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/IndentingXMLStreamWriterTest.java @@ -6,6 +6,7 @@ package gov.nist.secauto.metaschema.schemagen.xml.impl; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -148,8 +149,8 @@ void testNamespaceDeclarations() throws XMLStreamException { // Just verify it doesn't throw and produces valid output String result = sw.toString(); - assertEquals(true, result.contains(" ")); + assertTrue(result.contains(" ")); } @Test @@ -310,7 +311,7 @@ void testCDataNotIndented() throws XMLStreamException { } String result = sw.toString(); - assertEquals(true, result.contains(" chars]]>")); + assertTrue(result.contains(" chars]]>")); } @Test @@ -402,7 +403,7 @@ void testDocumentationWithParagraphs() throws XMLStreamException { String result = sw.toString(); // The p element should be indented as element-only content // But the text inside p should not have added whitespace - assertEquals(true, result.contains("This is documentation.")); + assertTrue(result.contains("This is documentation.")); } @Test @@ -425,7 +426,7 @@ void testDocumentationWithBoldInline() throws XMLStreamException { String result = sw.toString(); // No whitespace corruption in mixed content - assertEquals(true, result.contains("Text with bold content.")); + assertTrue(result.contains("Text with bold content.")); } @Test @@ -538,7 +539,7 @@ void testTextAfterChildElement() throws XMLStreamException { // Once text is written to parent, subsequent content should not be indented String result = sw.toString(); // The child was indented, but after the text, parent is in mixed content mode - assertEquals(true, result.contains("parent text after child")); + assertTrue(result.contains("parent text after child")); } @Test diff --git a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java index 8efa4d7c4..a9d7db47c 100644 --- a/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java +++ b/schemagen/src/test/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoaderTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import gov.nist.secauto.metaschema.core.model.IModule; import gov.nist.secauto.metaschema.core.util.CollectionUtil; @@ -78,9 +79,9 @@ void testSimpleTypeQuery() { .collect(Collectors.toList()); // Check for some expected core datatypes - assertEquals(true, typeNames.contains("Base64Datatype"), + assertTrue(typeNames.contains("Base64Datatype"), "Should contain Base64Datatype"); - assertEquals(true, typeNames.contains("StringDatatype"), + assertTrue(typeNames.contains("StringDatatype"), "Should contain StringDatatype"); } @@ -108,7 +109,7 @@ void testAllChildrenQuery() { .distinct() .collect(Collectors.toList()); - assertEquals(true, elementNames.contains("simpleType"), + assertTrue(elementNames.contains("simpleType"), "Should contain simpleType elements"); } } From eb76c89a14abb3dd89109ce97f5bcd3b5f22d26d Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Wed, 31 Dec 2025 14:40:58 -0500 Subject: [PATCH 7/7] fix: add null check to getPrefixes for NamespaceContext contract --- .../secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java index 5451f2997..4d9002441 100644 --- a/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java +++ b/schemagen/src/main/java/gov/nist/secauto/metaschema/schemagen/xml/impl/XmlSchemaLoader.java @@ -196,6 +196,9 @@ public String getPrefix(String namespaceURI) { @Override public Iterator getPrefixes(String namespaceURI) { + if (namespaceURI == null) { + throw new IllegalArgumentException("namespaceURI cannot be null"); + } List prefixes = new ArrayList<>(); for (Map.Entry entry : prefixToNamespace.entrySet()) { if (namespaceURI.equals(entry.getValue())) {