diff --git a/openespi-common/JACKSON3_XML_MARSHALLING_TEST_PLAN.md b/openespi-common/JACKSON3_XML_MARSHALLING_TEST_PLAN.md
new file mode 100644
index 00000000..27a84ae3
--- /dev/null
+++ b/openespi-common/JACKSON3_XML_MARSHALLING_TEST_PLAN.md
@@ -0,0 +1,860 @@
+# Jackson 3 XML Marshalling Test Plan
+
+**Branch:** `feature/jackson3-xml-marshalling-tests`
+**Date:** 2026-01-04
+**Purpose:** Update all XML marshalling tests to use Jackson 3 XmlMapper with JAXB annotations
+
+---
+
+## Executive Summary
+
+This plan addresses the gap identified during Multi-Phase Schema Compliance review:
+- **Current State**: XML marshalling tests use JAXB (`JAXBContext`, `Marshaller`, `Unmarshaller`)
+- **Target State**: Tests should use Jackson 3 (`XmlMapper`) to match production code
+- **Production Code**: Uses Jackson 3 XmlMapper with JAXB annotations (hybrid approach)
+- **Test Code**: Still uses pure JAXB (misalignment)
+
+---
+
+## Issues Discovered During Review
+
+### Issue #1: DtoExportServiceImpl Atom Metadata Extraction
+
+**File:** `openespi-common/src/main/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImpl.java:189-195`
+
+**Problem:** `createAtomEntry()` does NOT extract Atom metadata from entities
+
+**Current Behavior:**
+```java
+public AtomEntryDto createAtomEntry(String title, Object resource) {
+ return new AtomEntryDto(
+ UUID.randomUUID().toString(), // ❌ Generates NEW Version-4 UUID (random)
+ title, // ❌ Uses parameter string
+ resource // DTO resource
+ );
+}
+```
+
+**Issues:**
+- ❌ Atom ``: Generates new **Version-4** (random) UUID instead of using `entity.getId()` **(Version-5 UUID)**
+- ❌ Atom ``: Uses hardcoded parameter string instead of `entity.getDescription()`
+- ❌ Atom `` elements: Set to NULL instead of extracting from `entity.selfLink`, `upLink`, `relatedLinks`
+- ✅ Atom ``/``: Uses NOW (certified approach - correct)
+
+**CRITICAL:** ESPI requires **Version-5 UUIDs** (deterministic, based on namespace + name) NOT Version-4 (random)
+- Version-4: `UUID.randomUUID()` - generates random UUID (version field = `4`)
+- Version-5: `UUID.nameUUIDFromBytes()` - generates deterministic UUID from name (version field = `5`)
+
+**Expected Behavior:**
+```xml
+
+ urn:uuid:48c2a019-5598-5e16-b0f9-49e4ff27f5fb
+ Front Electric Meter
+ 2026-01-04T12:34:56Z
+ 2026-01-04T12:34:56Z
+
+
+
+
+ ...
+
+
+```
+
+**See:** `ATOM_METADATA_EXTRACTION_VERIFICATION.md` (verification document from review)
+
+**Action:** Create separate GitHub issue for DtoExportServiceImpl enhancement
+
+---
+
+### Issue #2: Test Code Uses Version-4 UUIDs
+
+**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImplTest.java:118`
+
+**Problem:** Test helper method creates entity with **Version-4 UUID** instead of **Version-5**
+
+**Current Test Code:**
+```java
+UsagePointEntity usagePointEntity = new UsagePointEntity();
+usagePointEntity.setId(UUID.fromString("48C2A019-5598-4E16-B0F9-49E4FF27F5FB"));
+// ↑
+// Version-4 (should be 5)
+```
+
+**UUID Analysis:**
+- Format: `48c2a019-5598-4e16-b0f9-49e4ff27f5fb`
+- Version field (3rd group, 1st char): `4e16` → **Version-4**
+- Should be: `48c2a019-5598-5e16-b0f9-49e4ff27f5fb` → **Version-5**
+
+**Impact:** Tests are not validating ESPI compliance requirement for Version-5 UUIDs
+
+**Fix Required:**
+1. Update test entity UUIDs to use Version-5 format
+2. Add assertions to validate UUID version field
+3. Document proper Version-5 UUID generation using `EspiIdGeneratorService`
+
+---
+
+### Issue #3: No Jackson 3 XML Marshalling Tests
+
+**Current Test Inventory:**
+
+| Test File | Technology | Status | Issue |
+|-----------|-----------|---------|-------|
+| **TimeConfigurationDtoTest.java** | JAXB | ✅ Active | Uses JAXBContext, not Jackson 3 |
+| **SimpleXmlMarshallingTest.java** | JAXB | ❌ @Disabled | Comment: "refactor to use Jackson for marshalling" |
+| **XmlDebugTest.java** | JAXB | ✅ Active | Uses JAXBContext, not Jackson 3 |
+| **DtoExportServiceImplTest.java** | Jackson 3 (indirect) | ⚠️ No assertions | Just prints output, no validation |
+
+**Gap:** No tests validate Jackson 3 XmlMapper XML marshalling/unmarshalling
+
+**Production Code Uses:**
+```java
+// DtoExportServiceImpl.java:154-172
+private XmlMapper createXmlMapper() {
+ AnnotationIntrospector intr = XmlAnnotationIntrospector.Pair.instance(
+ new JakartaXmlBindAnnotationIntrospector(),
+ new JacksonAnnotationIntrospector()
+ );
+
+ return XmlMapper.xmlBuilder()
+ .annotationIntrospector(intr)
+ .addModule(new JakartaXmlBindAnnotationModule()
+ .setNonNillableInclusion(JsonInclude.Include.NON_EMPTY))
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
+ .defaultDateFormat(new StdDateFormat())
+ .build();
+}
+```
+
+**Tests Should Use:** Same XmlMapper configuration to validate production code
+
+---
+
+## Test Output Location
+
+### DtoExportServiceImplTest Output
+
+**File:** `openespi-common/src/test/resources/sample-xml/testdata.xml`
+
+**Current Output (excerpt):**
+```xml
+
+ urn:uuid:fa404785-1dc7-5aab-952d-14f8841df8de
+
+
+
+
+ 86400
+ 1641099600
+
+
+
+ 86400
+ 1641099600
+
+ 3880
+
+
+
+ 2022-01-02T06:00:00Z
+ 2022-05-19T21:44:50Z
+
+
+```
+
+**Issues Visible in Output:**
+- Version-4 random UUID (should be Version-5 from entity)
+- Empty `` element
+- No `` elements
+
+---
+
+## UUID Version Requirements
+
+### ESPI UUID Specification
+
+**NAESB ESPI Standard requires Version-5 UUIDs:**
+- **Version-5**: Deterministic UUID based on namespace + name (SHA-1 hash)
+- **Format**: `xxxxxxxx-xxxx-5xxx-xxxx-xxxxxxxxxxxx` (note the '5' in the version field)
+- **Generation**: Based on resource's `self` link href
+
+**Version Identification:**
+```
+UUID Format: xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx
+ ↑
+ Version field (first hex digit of 3rd group)
+
+Version-4 example: 48c2a019-5598-4e16-b0f9-49e4ff27f5fb
+ ↑
+ Version = 4 (random)
+
+Version-5 example: 48c2a019-5598-5e16-b0f9-49e4ff27f5fb
+ ↑
+ Version = 5 (deterministic)
+```
+
+**Version-4 (WRONG):**
+```java
+UUID.randomUUID() // ❌ Generates random Version-4 UUID
+// Example: fa404785-1dc7-4aab-952d-14f8841df8de
+// ↑ version = 4
+```
+
+**Version-5 (CORRECT):**
+```java
+// Entity already has Version-5 UUID in id field
+entity.getId() // ✅ Returns Version-5 UUID from database
+// Example: 48c2a019-5598-5e16-b0f9-49e4ff27f5fb
+// ↑ version = 5
+```
+
+**Generation via EspiIdGeneratorService:**
+```java
+// IdentifiedObject.java:179-184
+public void generateEspiCompliantId(EspiIdGeneratorService idGeneratorService) {
+ if (selfLink != null && selfLink.getHref() != null) {
+ UUID espiId = idGeneratorService.generateEspiId(selfLink.getHref());
+ setId(espiId); // ✅ Sets Version-5 UUID
+ }
+}
+```
+
+---
+
+## Recommended Assertions for DtoExportServiceImplTest
+
+### Current Test Code
+
+**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImplTest.java:48-69`
+
+```java
+@Test
+void export_atom_feed_test() throws IOException {
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntryDto = getUsagePointEntry(now);
+ AtomEntryDto meterReadingEntryDto = getMeeterReadingEntryDto(now);
+ AtomEntryDto readingEntry = getReadingEntryDto(now);
+ AtomEntryDto intervalBlockEntry = getIntervlBlockEntryDto(now);
+
+ AtomFeedDto atomFeedDto = new AtomFeedDto("urn:uuid:15B0A4ED-CCF4-4521-A0A1-9FF650EC8A6B",
+ "Green Button Subscription Feed", now, now, null,
+ List.of(usagePointEntryDto, meterReadingEntryDto, readingEntry, intervalBlockEntry));
+
+ try (OutputStream stream = new ByteArrayOutputStream()) {
+ // Commented out due to conflict in IntervalReadingDto which cannot be fixed in this task
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ System.out.println(stream.toString()); // ❌ Only prints - no validation
+ }
+}
+```
+
+**Problem:** No assertions - just prints output
+
+---
+
+### Recommended Assertions
+
+#### 1. XML Structure Assertions
+
+```java
+@Test
+@DisplayName("Should export Atom feed with valid XML structure")
+void shouldExportAtomFeedWithValidXmlStructure() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:15B0A4ED-CCF4-4521-A0A1-9FF650EC8A6B",
+ "Green Button Subscription Feed",
+ now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - XML Declaration
+ assertThat(xml).startsWith("");
+
+ // Assert - Root element
+ assertThat(xml).contains("");
+
+ // Assert - Feed metadata
+ assertThat(xml).contains("urn:uuid:15B0A4ED-CCF4-4521-A0A1-9FF650EC8A6B");
+ assertThat(xml).contains("Green Button Subscription Feed");
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - Entry structure
+ assertThat(xml).contains("");
+
+ // Assert - ESPI namespace in content
+ assertThat(xml).contains("xmlns:espi=\"http://naesb.org/espi\"");
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+}
+```
+
+---
+
+#### 2. Atom Entry Metadata Assertions
+
+```java
+@Test
+@DisplayName("Should export Atom entry with proper metadata elements")
+void shouldExportAtomEntryWithProperMetadata() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:feed-id", "Test Feed", now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Entry has required Atom elements
+ assertThat(xml).contains("urn:uuid:[0-9a-f-]{36}"); // UUID format
+ assertThat(xml).contains("
"); // Should have title element
+ assertThat(xml).contains(""); // ISO 8601 timestamp
+ assertThat(xml).contains(""); // ISO 8601 timestamp
+
+ // Assert - Timestamps are ISO 8601 format
+ assertThat(xml).containsPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}");
+ assertThat(xml).containsPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}");
+}
+```
+
+---
+
+#### 3. UUID Version-5 Validation (Currently Failing - Expected)
+
+```java
+@Test
+@DisplayName("Should use Version-5 UUIDs for Atom entry IDs")
+@Disabled("Known issue: DtoExportServiceImpl generates Version-4 UUIDs instead of using entity Version-5 UUIDs - see GitHub issue #XX")
+void shouldUseVersion5UuidsForAtomEntryIds() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ // NOTE: getUsagePointEntry() currently creates entity with Version-4 UUID (line 118)
+ // This needs to be updated to use Version-5 UUID
+ // Expected Version-5: 48c2a019-5598-5e16-b0f9-49e4ff27f5fb (note '5' in version field)
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:feed-id", "Test Feed", now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Should use entity's Version-5 UUID (after test data is fixed)
+ assertThat(xml).contains("urn:uuid:48c2a019-5598-5e16-b0f9-49e4ff27f5fb");
+
+ // Assert - UUID version field should be '5' (4th group, 1st char)
+ // Format: xxxxxxxx-xxxx-5xxx-xxxx-xxxxxxxxxxxx
+ // ↑ version bit = 5
+ assertThat(xml).containsPattern("urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}");
+
+ // Assert - Should NOT contain Version-4 UUIDs (version bit = 4)
+ assertThat(xml).doesNotContainPattern("urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}");
+}
+```
+
+---
+
+#### 4. Atom Link Assertions (Currently Failing - Expected)
+
+```java
+@Test
+@DisplayName("Should export Atom entry with link elements")
+@Disabled("Known issue: DtoExportServiceImpl does not extract links from entities - see GitHub issue #XX")
+void shouldExportAtomEntryWithLinkElements() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:feed-id", "Test Feed", now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Entry should have links
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - UsagePoint fields
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+}
+```
+
+---
+
+#### 6. Round-Trip Marshalling Assertion
+
+```java
+@Test
+@DisplayName("Should support round-trip marshalling/unmarshalling")
+void shouldSupportRoundTripMarshalling() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto original = getUsagePointEntry(now);
+ AtomFeedDto originalFeed = new AtomFeedDto(
+ "urn:uuid:feed-id", "Test Feed", now, now, null,
+ List.of(original)
+ );
+
+ // Act - Marshal to XML
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(originalFeed, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Act - Unmarshal back from XML
+ XmlMapper xmlMapper = createTestXmlMapper();
+ AtomFeedDto roundTrip = xmlMapper.readValue(xml, AtomFeedDto.class);
+
+ // Assert - Feed metadata preserved
+ assertThat(roundTrip.id()).isEqualTo(originalFeed.id());
+ assertThat(roundTrip.title()).isEqualTo(originalFeed.title());
+
+ // Assert - Entry count preserved
+ assertThat(roundTrip.entries()).hasSize(originalFeed.entries().size());
+}
+
+private XmlMapper createTestXmlMapper() {
+ AnnotationIntrospector intr = XmlAnnotationIntrospector.Pair.instance(
+ new JakartaXmlBindAnnotationIntrospector(),
+ new JacksonAnnotationIntrospector()
+ );
+
+ return XmlMapper.xmlBuilder()
+ .annotationIntrospector(intr)
+ .addModule(new JakartaXmlBindAnnotationModule()
+ .setNonNillableInclusion(JsonInclude.Include.NON_EMPTY))
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
+ .defaultDateFormat(new StdDateFormat())
+ .build();
+}
+```
+
+---
+
+#### 7. XSD Schema Validation Assertion
+
+```java
+@Test
+@DisplayName("Should generate XML that validates against ESPI 4.0 XSD schema")
+void shouldGenerateXmlThatValidatesAgainstEspiXsd() throws Exception {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:feed-id", "Test Feed", now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act - Generate XML
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Validate against XSD
+ SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+ // Load ESPI 4.0 XSD schema
+ URL xsdUrl = getClass().getClassLoader().getResource("schema/ESPI_4.0/espi.xsd");
+ assertThat(xsdUrl).isNotNull();
+
+ Schema schema = schemaFactory.newSchema(xsdUrl);
+ Validator validator = schema.newValidator();
+
+ // Validate XML against schema
+ Source xmlSource = new StreamSource(new StringReader(xml));
+ assertThatCode(() -> validator.validate(xmlSource))
+ .doesNotThrowAnyException();
+}
+```
+
+---
+
+## Test Data Fix Required
+
+### Update Test UUIDs to Version-5
+
+**File:** `DtoExportServiceImplTest.java`
+
+**Current (line 118):**
+```java
+UsagePointEntity usagePointEntity = new UsagePointEntity();
+usagePointEntity.setId(UUID.fromString("48C2A019-5598-4E16-B0F9-49E4FF27F5FB"));
+// ↑ Version-4
+```
+
+**Fix Option 1: Manual Version-5 UUID**
+```java
+UsagePointEntity usagePointEntity = new UsagePointEntity();
+usagePointEntity.setId(UUID.fromString("48C2A019-5598-5E16-B0F9-49E4FF27F5FB"));
+// ↑ Changed to Version-5
+```
+
+**Fix Option 2: Generate Using EspiIdGeneratorService (Preferred)**
+```java
+UsagePointEntity usagePointEntity = new UsagePointEntity();
+usagePointEntity.setSelfLink(new LinkType("self", "/espi/1_1/resource/UsagePoint/48C2A019"));
+usagePointEntity.generateEspiCompliantId(espiIdGeneratorService);
+// Generates Version-5 UUID based on selfLink.href
+```
+
+**Apply to All Test Entities:**
+- UsagePoint (line 118)
+- MeterReading (line 106-112)
+- ReadingType (line 92-94)
+- IntervalBlock (line 80-81)
+
+---
+
+## Test Conversion Plan
+
+### Step 1: Update TimeConfigurationDtoTest to Use Jackson 3
+
+**Current:** Uses JAXB (`JAXBContext`, `Marshaller`, `Unmarshaller`)
+**Target:** Use Jackson 3 (`XmlMapper`)
+
+**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDtoTest.java`
+
+**Changes Required:**
+
+#### Before (JAXB):
+```java
+import jakarta.xml.bind.JAXBContext;
+import jakarta.xml.bind.JAXBException;
+import jakarta.xml.bind.Marshaller;
+import jakarta.xml.bind.Unmarshaller;
+
+@BeforeEach
+void setUp() throws JAXBException {
+ jaxbContext = JAXBContext.newInstance(TimeConfigurationDto.class);
+
+ marshaller = jaxbContext.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+ marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
+
+ unmarshaller = jaxbContext.createUnmarshaller();
+}
+```
+
+#### After (Jackson 3):
+```java
+import tools.jackson.databind.AnnotationIntrospector;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
+import tools.jackson.dataformat.xml.XmlAnnotationIntrospector;
+import tools.jackson.dataformat.xml.XmlMapper;
+import tools.jackson.dataformat.xml.XmlWriteFeature;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.util.StdDateFormat;
+import static org.assertj.core.api.Assertions.assertThat;
+
+private XmlMapper xmlMapper;
+
+@BeforeEach
+void setUp() {
+ AnnotationIntrospector intr = XmlAnnotationIntrospector.Pair.instance(
+ new JakartaXmlBindAnnotationIntrospector(),
+ new JacksonAnnotationIntrospector()
+ );
+
+ xmlMapper = XmlMapper.xmlBuilder()
+ .annotationIntrospector(intr)
+ .addModule(new JakartaXmlBindAnnotationModule()
+ .setNonNillableInclusion(JsonInclude.Include.NON_EMPTY))
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
+ .defaultDateFormat(new StdDateFormat())
+ .build();
+}
+
+@Test
+@DisplayName("Should marshal TimeConfigurationDto with realistic timezone data")
+void shouldMarshalTimeConfigurationWithRealisticData() throws Exception {
+ // Arrange
+ TimeConfigurationDto timeConfig = new TimeConfigurationDto(
+ null, // id (transient)
+ "urn:uuid:550e8400-e29b-41d4-a716-446655440000", // uuid
+ new byte[]{0x01, 0x0B, 0x05, 0x00, 0x02, 0x00}, // dstEndRule
+ 3600L, // dstOffset
+ new byte[]{0x01, 0x03, 0x02, 0x00, 0x02, 0x00}, // dstStartRule
+ -28800L // tzOffset (UTC-8)
+ );
+
+ // Act
+ String xml = xmlMapper.writeValueAsString(timeConfig); // Jackson 3
+
+ // Assert
+ assertThat(xml).contains("TimeConfiguration");
+ assertThat(xml).contains("http://naesb.org/espi");
+ assertThat(xml).contains("mRID");
+ assertThat(xml).contains("550e8400-e29b-41d4-a716-446655440000");
+ assertThat(xml).contains("-28800");
+ assertThat(xml).contains("3600");
+}
+```
+
+---
+
+### Step 2: Enable and Update SimpleXmlMarshallingTest
+
+**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/SimpleXmlMarshallingTest.java`
+
+**Changes Required:**
+
+1. **Remove @Disabled annotation** (line 41)
+2. **Replace JAXB imports** with Jackson 3 imports
+3. **Update setUp() method** to create XmlMapper
+4. **Update all test methods** to use `xmlMapper.writeValueAsString()` and `xmlMapper.readValue()`
+5. **Rename class** to `Jackson3XmlMarshallingTest`
+
+---
+
+### Step 3: Update XmlDebugTest
+
+**File:** `openespi-common/src/test/java/org/greenbuttonalliance/espi/common/XmlDebugTest.java`
+
+**Changes Required:**
+- Same as TimeConfigurationDtoTest (replace JAXB with Jackson 3)
+- Keep debug output but add assertions
+
+---
+
+### Step 4: Fix Test Data UUIDs
+
+**File:** `DtoExportServiceImplTest.java`
+
+**Changes Required:**
+1. Update all test entity UUIDs from Version-4 to Version-5
+2. Use `EspiIdGeneratorService.generateEspiId()` where appropriate
+3. Document UUID version requirements in test comments
+
+---
+
+### Step 5: Add Assertions to DtoExportServiceImplTest
+
+**File:** `DtoExportServiceImplTest.java`
+
+**Changes Required:**
+1. Add all recommended assertions from above
+2. Extract XmlMapper creation to helper method
+3. Add round-trip tests
+4. Add XSD validation tests
+5. Add Version-5 UUID validation test (mark @Disabled until DtoExportServiceImpl fixed)
+6. Mark link-related tests as @Disabled with reference to GitHub issue
+
+---
+
+## Implementation Checklist
+
+- [ ] **Step 1**: Update TimeConfigurationDtoTest to use Jackson 3
+ - [ ] Replace JAXB imports with Jackson 3 imports
+ - [ ] Update setUp() to create XmlMapper
+ - [ ] Update all test methods to use xmlMapper
+ - [ ] Run tests and verify all pass
+
+- [ ] **Step 2**: Enable and update SimpleXmlMarshallingTest
+ - [ ] Remove @Disabled annotation
+ - [ ] Rename to Jackson3XmlMarshallingTest
+ - [ ] Replace JAXB with Jackson 3
+ - [ ] Run tests and verify all pass
+
+- [ ] **Step 3**: Update XmlDebugTest
+ - [ ] Replace JAXB with Jackson 3
+ - [ ] Add assertions to validate output
+ - [ ] Run tests and verify passes
+
+- [ ] **Step 4**: Fix test data UUIDs to Version-5
+ - [ ] Update UsagePoint UUID (line 118): `4E16` → `5E16`
+ - [ ] Update MeterReading UUID if present
+ - [ ] Update ReadingType UUID if present
+ - [ ] Update IntervalBlock UUID if present
+ - [ ] Add UUID version validation helpers
+ - [ ] Document UUID requirements in test comments
+
+- [ ] **Step 5**: Add assertions to DtoExportServiceImplTest
+ - [ ] Add XML structure assertions
+ - [ ] Add Atom metadata assertions
+ - [ ] Add ESPI content assertions
+ - [ ] Add round-trip marshalling test
+ - [ ] Add XSD validation test
+ - [ ] Add Version-5 UUID validation test (mark @Disabled)
+ - [ ] Mark link tests as @Disabled with GitHub issue reference
+ - [ ] Run tests and verify all pass (except disabled)
+
+- [ ] **Step 6**: Create GitHub issue for DtoExportServiceImpl enhancement
+ - [ ] Document Atom metadata extraction issue (id, title, links)
+ - [ ] Document Version-5 UUID requirement
+ - [ ] Reference ATOM_METADATA_EXTRACTION_VERIFICATION.md
+ - [ ] Link to disabled test cases
+
+- [ ] **Step 7**: Update documentation
+ - [ ] Update MULTI_PHASE_SCHEMA_COMPLIANCE_PLAN.md if needed
+ - [ ] Add this plan to openespi-common/docs/
+
+- [ ] **Step 8**: Commit, push, create PR
+ - [ ] Stage changes: `git add .`
+ - [ ] Commit: `git commit -m "feat: update XML marshalling tests to use Jackson 3 and fix UUID versions"`
+ - [ ] Push: `git push origin feature/jackson3-xml-marshalling-tests`
+ - [ ] Create PR
+
+---
+
+## Required Dependencies
+
+Verify these are in `openespi-common/pom.xml`:
+
+```xml
+
+
+ tools.jackson.dataformat
+ jackson-dataformat-xml
+ 3.0.3
+
+
+
+
+ tools.jackson.module
+ jackson-module-jakarta-xmlbind-annotations
+ 3.0.3
+
+
+
+
+ org.assertj
+ assertj-core
+ test
+
+```
+
+---
+
+## Expected Test Output Location
+
+After running tests, XML output samples are stored in:
+
+**Location:** `openespi-common/src/test/resources/sample-xml/testdata.xml`
+
+**Current Content:** Atom feed with IntervalBlock, IntervalReading, UsagePoint entries
+
+**Access in Tests:**
+```java
+URL xmlUrl = getClass().getClassLoader().getResource("sample-xml/testdata.xml");
+```
+
+**Note:** This file is generated by `DtoExportServiceImplTest` but not currently validated by assertions.
+
+---
+
+## Success Criteria
+
+1. ✅ All XML marshalling tests use Jackson 3 XmlMapper (not JAXB)
+2. ✅ TimeConfigurationDtoTest updated and passing
+3. ✅ SimpleXmlMarshallingTest enabled, updated, and passing
+4. ✅ XmlDebugTest updated and passing
+5. ✅ All test entity UUIDs converted to Version-5
+6. ✅ DtoExportServiceImplTest has comprehensive assertions
+7. ✅ All tests validate Jackson 3 processes JAXB annotations correctly
+8. ✅ XSD validation tests confirm ESPI 4.0 schema compliance
+9. ✅ Round-trip marshalling tests confirm data integrity
+10. ✅ Version-5 UUID validation test created (disabled until DtoExportServiceImpl fixed)
+11. ✅ Test output location documented
+12. ✅ GitHub issue created for DtoExportServiceImpl enhancement
+
+---
+
+**Author:** Claude Sonnet 4.5
+**Date:** 2026-01-04
+**Branch:** `feature/jackson3-xml-marshalling-tests`
+**Related Documents:**
+- MULTI_PHASE_SCHEMA_COMPLIANCE_PLAN.md
+- ATOM_METADATA_EXTRACTION_VERIFICATION.md (to be created)
+- DTO_APPROACH_COMPARISON.md
+
+**Test Data UUID Corrections:**
+```
+Current (Version-4): 48C2A019-5598-4E16-B0F9-49E4FF27F5FB
+ ↑ version = 4
+Fixed (Version-5): 48C2A019-5598-5E16-B0F9-49E4FF27F5FB
+ ↑ version = 5
+```
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java
index ee89810c..7ae9dce7 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java
@@ -164,12 +164,15 @@ public void setTzOffset(Long tzOffset) {
}
// Utility methods
+ // TEMPORARY: @XmlTransient annotations prevent serialization with XmlAccessType.PROPERTY
+ // TODO: Remove when converting to record with XmlAccessType.FIELD (see issue #61)
/**
* Gets the timezone offset in hours.
*
* @return timezone offset in hours, or null if not set
*/
+ @XmlTransient
public Double getTzOffsetInHours() {
return tzOffset != null ? tzOffset / 3600.0 : null;
}
@@ -179,6 +182,7 @@ public Double getTzOffsetInHours() {
*
* @return DST offset in hours, or null if not set
*/
+ @XmlTransient
public Double getDstOffsetInHours() {
return dstOffset != null ? dstOffset / 3600.0 : null;
}
@@ -188,6 +192,7 @@ public Double getDstOffsetInHours() {
*
* @return total offset in seconds including DST
*/
+ @XmlTransient
public Long getEffectiveOffset() {
Long base = tzOffset != null ? tzOffset : 0L;
Long dst = dstOffset != null ? dstOffset : 0L;
@@ -199,6 +204,7 @@ public Long getEffectiveOffset() {
*
* @return total offset in hours including DST
*/
+ @XmlTransient
public Double getEffectiveOffsetInHours() {
return getEffectiveOffset() / 3600.0;
}
@@ -208,6 +214,7 @@ public Double getEffectiveOffsetInHours() {
*
* @return true if DST rules are present, false otherwise
*/
+ @XmlTransient
public boolean hasDstRules() {
return dstStartRule != null && dstStartRule.length > 0 &&
dstEndRule != null && dstEndRule.length > 0;
@@ -218,6 +225,7 @@ public boolean hasDstRules() {
*
* @return true if DST offset is defined and non-zero, false otherwise
*/
+ @XmlTransient
public boolean isDstActive() {
return dstOffset != null && dstOffset != 0;
}
diff --git a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java
index d1e675b9..dfaefa36 100644
--- a/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java
+++ b/openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java
@@ -287,36 +287,40 @@ public UsagePointDto(String uuid, String description, ServiceCategory serviceCat
/**
* Generates the default self href for a usage point.
- *
+ *
* @return default self href
*/
+ @XmlTransient
public String generateSelfHref() {
return uuid != null ? "/espi/1_1/resource/UsagePoint/" + uuid : null;
}
-
+
/**
* Generates the default up href for a usage point.
- *
+ *
* @return default up href
*/
+ @XmlTransient
public String generateUpHref() {
return "/espi/1_1/resource/UsagePoint";
}
/**
* Gets the total number of meter readings.
- *
+ *
* @return meter reading count
*/
+ @XmlTransient
public int getMeterReadingCount() {
return 0; // Temporarily disabled for compilation
}
-
+
/**
* Gets the total number of usage summaries.
- *
+ *
* @return usage summary count
*/
+ @XmlTransient
public int getUsageSummaryCount() {
return 0; // Temporarily disabled for compilation
}
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/Jackson3XmlMarshallingTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/Jackson3XmlMarshallingTest.java
new file mode 100644
index 00000000..f3a0447a
--- /dev/null
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/Jackson3XmlMarshallingTest.java
@@ -0,0 +1,245 @@
+/*
+ *
+ * Copyright (c) 2025 Green Button Alliance, Inc.
+ *
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.greenbuttonalliance.espi.common;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.greenbuttonalliance.espi.common.dto.usage.UsagePointDto;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import tools.jackson.databind.AnnotationIntrospector;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
+import tools.jackson.databind.util.StdDateFormat;
+import tools.jackson.dataformat.xml.XmlAnnotationIntrospector;
+import tools.jackson.dataformat.xml.XmlMapper;
+import tools.jackson.dataformat.xml.XmlWriteFeature;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * Jackson 3 XML marshalling tests to verify JAXB annotation processing with ESPI data.
+ * Tests marshal/unmarshal round-trip with realistic data structures using Jackson 3 XmlMapper.
+ */
+@DisplayName("Jackson 3 XML Marshalling Tests")
+class Jackson3XmlMarshallingTest {
+
+ private XmlMapper xmlMapper;
+
+ @BeforeEach
+ void setUp() {
+ // Initialize Jackson 3 XmlMapper with JAXB annotation support
+ AnnotationIntrospector intr = XmlAnnotationIntrospector.Pair.instance(
+ new JakartaXmlBindAnnotationIntrospector(),
+ new JacksonAnnotationIntrospector()
+ );
+
+ xmlMapper = XmlMapper.xmlBuilder()
+ .annotationIntrospector(intr)
+ .addModule(new JakartaXmlBindAnnotationModule()
+ .setNonNillableInclusion(JsonInclude.Include.NON_EMPTY))
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
+ .defaultDateFormat(new StdDateFormat())
+ .build();
+ }
+
+ @Test
+ @DisplayName("Should marshal UsagePointDto with realistic data")
+ void shouldMarshalUsagePointWithRealisticData() throws Exception {
+ // Create a UsagePointDto with realistic ESPI data
+ UsagePointDto usagePoint = new UsagePointDto(
+ "urn:uuid:test-usage-point",
+ "Residential Electric Service",
+ new byte[]{0x01, 0x04}, // Electricity consumer role flags
+ null, // serviceCategory
+ (short) 1, // Active status
+ null, null, null, null, // measurement fields
+ null, null, null, // reference fields
+ null, null, null // collection fields
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(usagePoint);
+
+ // Verify XML structure
+ assertThat(xml).contains("UsagePoint");
+ assertThat(xml).contains("http://naesb.org/espi");
+ assertThat(xml).contains("Residential Electric Service");
+ assertThat(xml).containsPattern("]*>1"); // May have xmlns attribute
+ }
+
+ @Test
+ @DisplayName("Should perform round-trip marshalling for UsagePointDto")
+ void shouldPerformRoundTripMarshallingForUsagePoint() throws Exception {
+ // Create original UsagePoint with comprehensive data
+ UsagePointDto original = new UsagePointDto(
+ "urn:uuid:commercial-gas-point",
+ "Commercial Gas Service",
+ new byte[]{0x02, 0x08}, // Gas consumer role flags
+ null, // serviceCategory
+ (short) 1, // Active status
+ null, null, null, null, // measurement fields
+ null, null, null, // reference fields
+ null, null, null // collection fields
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(original);
+
+ // Unmarshal back from XML using Jackson 3
+ UsagePointDto roundTrip = xmlMapper.readValue(xml, UsagePointDto.class);
+
+ // Verify data integrity survived round trip
+ assertThat(roundTrip.getDescription()).isEqualTo(original.getDescription());
+ assertThat(roundTrip.getStatus()).isEqualTo(original.getStatus());
+ assertThat(roundTrip.getRoleFlags()).isEqualTo(original.getRoleFlags());
+ }
+
+ @Test
+ @DisplayName("Should handle empty UsagePointDto without errors")
+ void shouldHandleEmptyUsagePointWithoutErrors() throws Exception {
+ // Create empty UsagePoint
+ UsagePointDto empty = new UsagePointDto(
+ null, null, null, null, null,
+ null, null, null, null,
+ null, null, null,
+ null, null, null
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(empty);
+
+ // Should still contain basic structure
+ assertThat(xml).contains("UsagePoint");
+ assertThat(xml).contains("http://naesb.org/espi");
+
+ // Unmarshal back using Jackson 3
+ UsagePointDto roundTrip = xmlMapper.readValue(xml, UsagePointDto.class);
+
+ // Should not throw exceptions
+ assertThat(roundTrip).isNotNull();
+ }
+
+ @Test
+ @DisplayName("Should handle null values gracefully")
+ void shouldHandleNullValuesGracefully() throws Exception {
+ // Create UsagePoint with some null values
+ UsagePointDto withNulls = new UsagePointDto(
+ "urn:uuid:test-nulls",
+ null, // Null description
+ null, // Null role flags
+ null, // serviceCategory
+ (short) 1, // Non-null status
+ null, null, null, null, // measurement fields
+ null, null, null, // reference fields
+ null, null, null // collection fields
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(withNulls);
+
+ // Unmarshal back using Jackson 3
+ UsagePointDto roundTrip = xmlMapper.readValue(xml, UsagePointDto.class);
+
+ // Verify nulls are preserved
+ assertThat(roundTrip.getDescription()).isNull();
+ assertThat(roundTrip.getRoleFlags()).isNull();
+ assertThat(roundTrip.getStatus()).isEqualTo(withNulls.getStatus());
+ }
+
+ @Test
+ @DisplayName("Should include proper XML namespaces")
+ void shouldIncludeProperXmlNamespaces() throws Exception {
+ // Create UsagePoint
+ UsagePointDto usagePoint = new UsagePointDto(
+ "urn:uuid:test-namespaces",
+ "Test Service",
+ null, null, null,
+ null, null, null, null,
+ null, null, null,
+ null, null, null
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(usagePoint);
+
+ // Verify namespace declarations
+ assertThat(xml).contains("xmlns");
+ assertThat(xml).contains("http://naesb.org/espi");
+
+ // Verify no legacy namespaces
+ assertThat(xml).doesNotContain("legacy");
+ assertThat(xml).doesNotContain("deprecated");
+ }
+
+ @Test
+ @DisplayName("Should marshal special characters correctly")
+ void shouldMarshalSpecialCharactersCorrectly() throws Exception {
+ // Create UsagePoint with special characters
+ UsagePointDto usagePoint = new UsagePointDto(
+ "urn:uuid:test-special-chars",
+ "Service & Co. \"Smart\" Meter",
+ null, null, null,
+ null, null, null, null,
+ null, null, null,
+ null, null, null
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(usagePoint);
+
+ // Verify XML escaping
+ assertThat(xml)
+ .satisfiesAnyOf(
+ s -> assertThat(s).contains("&"),
+ s -> assertThat(s).contains("Service & Co.")
+ );
+ assertThat(xml).contains("<Electric>"); // < is escaped, > in quoted text may not be
+
+ // Unmarshal back and verify data integrity using Jackson 3
+ UsagePointDto roundTrip = xmlMapper.readValue(xml, UsagePointDto.class);
+
+ assertThat(roundTrip.getDescription()).isEqualTo(usagePoint.getDescription());
+ }
+
+ @Test
+ @DisplayName("Should not throw exceptions during marshalling")
+ void shouldNotThrowExceptionsDuringMarshalling() {
+ // Create UsagePoint
+ UsagePointDto usagePoint = new UsagePointDto(
+ "urn:uuid:test-no-exceptions",
+ "Test Service",
+ null, null, null,
+ null, null, null, null,
+ null, null, null,
+ null, null, null
+ );
+
+ // Verify marshalling does not throw using Jackson 3
+ assertThatCode(() -> xmlMapper.writeValueAsString(usagePoint))
+ .doesNotThrowAnyException();
+ }
+}
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java
index 86aed859..5bd30f9e 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/MigrationVerificationTest.java
@@ -19,6 +19,7 @@
package org.greenbuttonalliance.espi.common;
+import com.fasterxml.jackson.annotation.JsonInclude;
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
import org.greenbuttonalliance.espi.common.domain.usage.UsagePointEntity;
import org.greenbuttonalliance.espi.common.domain.customer.entity.CustomerEntity;
@@ -28,15 +29,21 @@
import org.greenbuttonalliance.espi.common.dto.SummaryMeasurementDto;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
+import tools.jackson.databind.AnnotationIntrospector;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
+import tools.jackson.databind.util.StdDateFormat;
+import tools.jackson.dataformat.xml.XmlAnnotationIntrospector;
+import tools.jackson.dataformat.xml.XmlMapper;
+import tools.jackson.dataformat.xml.XmlWriteFeature;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
-import jakarta.xml.bind.JAXBContext;
-import jakarta.xml.bind.JAXBException;
-import jakarta.xml.bind.Marshaller;
-import java.io.StringWriter;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@@ -63,18 +70,31 @@ void jakartaValidationShouldWork() {
}
@Test
- @DisplayName("Jakarta XML Binding should work for DTOs")
- void jakartaXmlBindingShouldWork() throws JAXBException {
- JAXBContext context = JAXBContext.newInstance(UsagePointDto.class);
- Marshaller marshaller = context.createMarshaller();
-
+ @DisplayName("Jackson 3 XML with JAXB annotations should work for DTOs")
+ void jackson3XmlWithJaxbAnnotationsShouldWork() throws Exception {
+ // Production code uses Jackson 3 XmlMapper with JAXB annotation support
+ AnnotationIntrospector intr = XmlAnnotationIntrospector.Pair.instance(
+ new JakartaXmlBindAnnotationIntrospector(),
+ new JacksonAnnotationIntrospector()
+ );
+
+ XmlMapper xmlMapper = XmlMapper.xmlBuilder()
+ .annotationIntrospector(intr)
+ .addModule(new JakartaXmlBindAnnotationModule()
+ .setNonNillableInclusion(JsonInclude.Include.NON_EMPTY))
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
+ .defaultDateFormat(new StdDateFormat())
+ .build();
+
// Create a simple DTO without constructor arguments
UsagePointDto dto = new UsagePointDto();
-
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(dto, writer));
-
- String xml = writer.toString();
+
+ // Marshal using Jackson 3
+ String xml = assertDoesNotThrow(() -> xmlMapper.writeValueAsString(dto));
+
+ // Verify XML structure
assertTrue(xml.contains("UsagePoint"));
assertTrue(xml.contains("http://naesb.org/espi"));
}
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/SimpleXmlMarshallingTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/SimpleXmlMarshallingTest.java
deleted file mode 100644
index fa55be55..00000000
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/SimpleXmlMarshallingTest.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- *
- * Copyright (c) 2025 Green Button Alliance, Inc.
- *
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package org.greenbuttonalliance.espi.common;
-
-import jakarta.xml.bind.JAXBContext;
-import jakarta.xml.bind.JAXBException;
-import jakarta.xml.bind.Marshaller;
-import jakarta.xml.bind.Unmarshaller;
-import org.greenbuttonalliance.espi.common.dto.usage.UsagePointDto;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-import java.io.StringReader;
-import java.io.StringWriter;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * Simple XML marshalling tests to verify basic JAXB functionality with ESPI data.
- * Tests marshal/unmarshal round-trip with realistic data structures.
- */
-@Disabled // JT - todo, refactor to use Jackson for mashalling
-@DisplayName("Simple XML Marshalling Tests")
-class SimpleXmlMarshallingTest {
-
- private JAXBContext jaxbContext;
- private Marshaller marshaller;
- private Unmarshaller unmarshaller;
-
- @BeforeEach
- void setUp() throws JAXBException {
- // Initialize JAXB context for UsagePoint DTO
- jaxbContext = JAXBContext.newInstance(UsagePointDto.class);
-
- marshaller = jaxbContext.createMarshaller();
- marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
- marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
-
- unmarshaller = jaxbContext.createUnmarshaller();
- }
-
- @Test
- @DisplayName("Should marshal UsagePointDto with realistic data")
- void shouldMarshalUsagePointWithRealisticData() throws Exception {
- // Create a UsagePointDto record with realistic ESPI data
- UsagePointDto usagePoint = new UsagePointDto(
- "urn:uuid:test-usage-point",
- "Residential Electric Service",
- new byte[]{0x01, 0x04}, // Electricity consumer role flags
- null, // serviceCategory
- (short) 1, // Active status
- null, null, null, null, // measurement fields
- null, null, null, // reference fields
- null, null, null // collection fields
- );
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(usagePoint, writer));
-
- String xml = writer.toString();
-
- // Verify XML structure
- assertTrue(xml.contains("UsagePoint"), "XML should contain UsagePoint element");
- assertTrue(xml.contains("http://naesb.org/espi"), "XML should contain ESPI namespace");
- assertTrue(xml.contains("Residential Electric Service"), "XML should contain description");
- assertTrue(xml.contains("1"), "XML should contain status");
- }
-
- @Test
- @DisplayName("Should perform round-trip marshalling for UsagePointDto")
- void shouldPerformRoundTripMarshallingForUsagePoint() throws Exception {
- // Create original UsagePoint record with comprehensive data
- UsagePointDto original = new UsagePointDto(
- "urn:uuid:commercial-gas-point",
- "Commercial Gas Service",
- new byte[]{0x02, 0x08}, // Gas consumer role flags
- null, // serviceCategory
- (short) 1, // Active status
- null, null, null, null, // measurement fields
- null, null, null, // reference fields
- null, null, null // collection fields
- );
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- marshaller.marshal(original, writer);
- String xml = writer.toString();
-
- // Unmarshal back from XML
- StringReader reader = new StringReader(xml);
- UsagePointDto roundTrip = (UsagePointDto) unmarshaller.unmarshal(reader);
-
- // Verify data integrity survived round trip
- assertEquals(original.getDescription(), roundTrip.getDescription(),
- "Description should survive round trip");
- assertEquals(original.getStatus(), roundTrip.getStatus(),
- "Status should survive round trip");
- assertArrayEquals(original.getRoleFlags(), roundTrip.getRoleFlags(),
- "Role flags should survive round trip");
- }
-
- @Test
- @DisplayName("Should handle empty UsagePointDto without errors")
- void shouldHandleEmptyUsagePointWithoutErrors() throws Exception {
- // Create empty UsagePoint record
- UsagePointDto empty = new UsagePointDto(
- null, null, null, null, null,
- null, null, null, null,
- null, null, null,
- null, null, null
- );
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(empty, writer));
-
- String xml = writer.toString();
-
- // Should still contain basic structure
- assertTrue(xml.contains("UsagePoint"), "XML should contain UsagePoint element");
- assertTrue(xml.contains("http://naesb.org/espi"), "XML should contain ESPI namespace");
-
- // Unmarshal back
- StringReader reader = new StringReader(xml);
- UsagePointDto roundTrip = (UsagePointDto) unmarshaller.unmarshal(reader);
-
- // Should not throw exceptions
- assertNotNull(roundTrip, "Round trip should produce valid object");
- }
-
- @Test
- @DisplayName("Should handle null values gracefully")
- void shouldHandleNullValuesGracefully() throws Exception {
- // Create UsagePoint record with some null values
- UsagePointDto withNulls = new UsagePointDto(
- "urn:uuid:test-nulls",
- null, // Null description
- null, // Null role flags
- null, // serviceCategory
- (short) 1, // Non-null status
- null, null, null, null, // measurement fields
- null, null, null, // reference fields
- null, null, null // collection fields
- );
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(withNulls, writer));
-
- String xml = writer.toString();
-
- // Unmarshal back
- StringReader reader = new StringReader(xml);
- UsagePointDto roundTrip = (UsagePointDto) unmarshaller.unmarshal(reader);
-
- // Verify nulls are preserved
- assertNull(roundTrip.getDescription(), "Null description should be preserved");
- assertNull(roundTrip.getRoleFlags(), "Null role flags should be preserved");
- assertEquals(withNulls.getStatus(), roundTrip.getStatus(), "Non-null status should be preserved");
- }
-
- @Test
- @DisplayName("Should include proper XML namespaces")
- void shouldIncludeProperXmlNamespaces() throws Exception {
- // Create UsagePoint record
- UsagePointDto usagePoint = new UsagePointDto(
- "urn:uuid:test-namespaces",
- "Test Service",
- null, null, null,
- null, null, null, null,
- null, null, null,
- null, null, null
- );
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- marshaller.marshal(usagePoint, writer);
- String xml = writer.toString();
-
- // Verify namespace declarations
- assertTrue(xml.contains("xmlns") && xml.contains("http://naesb.org/espi"),
- "XML should contain ESPI namespace declaration");
-
- // Verify no legacy namespaces
- assertFalse(xml.contains("legacy"), "XML should not contain legacy references");
- assertFalse(xml.contains("deprecated"), "XML should not contain deprecated references");
- }
-
- @Test
- @DisplayName("Should marshal special characters correctly")
- void shouldMarshalSpecialCharactersCorrectly() throws Exception {
- // Create UsagePoint record with special characters
- UsagePointDto usagePoint = new UsagePointDto(
- "urn:uuid:test-special-chars",
- "Service & Co. \"Smart\" Meter",
- null, null, null,
- null, null, null, null,
- null, null, null,
- null, null, null
- );
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- marshaller.marshal(usagePoint, writer);
- String xml = writer.toString();
-
- // Verify XML escaping
- assertTrue(xml.contains("&") || xml.contains("Service & Co."),
- "Ampersands should be XML escaped");
- assertTrue(xml.contains("<") && xml.contains(">"),
- "Angle brackets should be XML escaped");
-
- // Unmarshal back and verify data integrity
- StringReader reader = new StringReader(xml);
- UsagePointDto roundTrip = (UsagePointDto) unmarshaller.unmarshal(reader);
-
- assertEquals(usagePoint.getDescription(), roundTrip.getDescription(),
- "Special characters should survive round trip");
- }
-}
\ No newline at end of file
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/XmlDebugTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/XmlDebugTest.java
index 8702e453..267cc3bb 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/XmlDebugTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/XmlDebugTest.java
@@ -19,38 +19,54 @@
package org.greenbuttonalliance.espi.common;
+import com.fasterxml.jackson.annotation.JsonInclude;
import org.greenbuttonalliance.espi.common.dto.usage.UsagePointDto;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import tools.jackson.databind.AnnotationIntrospector;
+import tools.jackson.databind.SerializationFeature;
+import tools.jackson.databind.cfg.DateTimeFeature;
+import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
+import tools.jackson.databind.util.StdDateFormat;
+import tools.jackson.dataformat.xml.XmlAnnotationIntrospector;
+import tools.jackson.dataformat.xml.XmlMapper;
+import tools.jackson.dataformat.xml.XmlWriteFeature;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
+import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule;
-import jakarta.xml.bind.JAXBContext;
-import jakarta.xml.bind.JAXBException;
-import jakarta.xml.bind.Marshaller;
-
-import java.io.StringWriter;
-
-import static org.junit.jupiter.api.Assertions.*;
+import static org.assertj.core.api.Assertions.assertThat;
/**
- * Debug test to see what XML is actually being generated.
+ * Debug test to see what XML is actually generated by Jackson 3 XmlMapper.
+ * Useful for validating XML structure and namespace handling.
*/
@DisplayName("XML Debug Test")
class XmlDebugTest {
- private JAXBContext jaxbContext;
- private Marshaller marshaller;
+ private XmlMapper xmlMapper;
@BeforeEach
- void setUp() throws JAXBException {
- jaxbContext = JAXBContext.newInstance(UsagePointDto.class);
- marshaller = jaxbContext.createMarshaller();
- marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
- marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
+ void setUp() {
+ // Initialize Jackson 3 XmlMapper with JAXB annotation support
+ AnnotationIntrospector intr = XmlAnnotationIntrospector.Pair.instance(
+ new JakartaXmlBindAnnotationIntrospector(),
+ new JacksonAnnotationIntrospector()
+ );
+
+ xmlMapper = XmlMapper.xmlBuilder()
+ .annotationIntrospector(intr)
+ .addModule(new JakartaXmlBindAnnotationModule()
+ .setNonNillableInclusion(JsonInclude.Include.NON_EMPTY))
+ .enable(SerializationFeature.INDENT_OUTPUT)
+ .enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
+ .disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
+ .defaultDateFormat(new StdDateFormat())
+ .build();
}
@Test
- @DisplayName("Debug: See what XML is actually generated")
+ @DisplayName("Debug: See what XML is actually generated by Jackson 3")
void debugXmlOutput() throws Exception {
// Create a simple UsagePointDto
UsagePointDto usagePoint = new UsagePointDto(
@@ -63,21 +79,107 @@ void debugXmlOutput() throws Exception {
null, null, null, // reference fields
null, null, null // collection fields
);
-
- // Marshal to XML
- StringWriter writer = new StringWriter();
- marshaller.marshal(usagePoint, writer);
- String xml = writer.toString();
-
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(usagePoint);
+
// Print the actual XML for debugging
- System.out.println("Generated XML:");
- System.out.println("==============");
+ System.out.println("Generated XML (Jackson 3):");
+ System.out.println("==========================");
System.out.println(xml);
- System.out.println("==============");
-
- // Basic checks
- assertNotNull(xml);
- assertFalse(xml.trim().isEmpty());
- assertTrue(xml.contains("UsagePoint"), "Should contain UsagePoint element");
+ System.out.println("==========================");
+
+ // Comprehensive assertions
+ assertThat(xml).isNotNull();
+ assertThat(xml.trim()).isNotEmpty();
+
+ // Validate root element
+ assertThat(xml).contains("UsagePoint");
+
+ // Validate ESPI namespace
+ assertThat(xml).contains("http://naesb.org/espi");
+
+ // Validate content
+ assertThat(xml).contains("Debug Service");
+ assertThat(xml).containsPattern("]*>1");
+ assertThat(xml).contains("01"); // roleFlags as hex
+
+ // Validate XML structure
+ assertThat(xml).startsWith(""); // Trim whitespace
+
+ // Validate no utility methods are serialized (should have @XmlTransient)
+ assertThat(xml).doesNotContain("meterReadingCount");
+ assertThat(xml).doesNotContain("usageSummaryCount");
+ assertThat(xml).doesNotContain("generateSelfHref");
+ assertThat(xml).doesNotContain("generateUpHref");
+ }
+
+ @Test
+ @DisplayName("Debug: Complex UsagePoint with all fields")
+ void debugComplexUsagePoint() throws Exception {
+ // Create UsagePoint with more fields populated
+ UsagePointDto usagePoint = new UsagePointDto(
+ "urn:uuid:complex-test",
+ "Complex Service with Special & Characters ",
+ new byte[]{0x01, 0x02, 0x03, 0x04},
+ null, // serviceCategory
+ (short) 2,
+ null, null, null, null,
+ null, null, null,
+ null, null, null
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(usagePoint);
+
+ // Print for debugging
+ System.out.println("\nComplex XML (Jackson 3):");
+ System.out.println("========================");
+ System.out.println(xml);
+ System.out.println("========================\n");
+
+ // Validate XML escaping
+ assertThat(xml).contains("&"); // & is escaped
+ assertThat(xml).contains("<"); // < is escaped
+
+ // Validate hex encoding of roleFlags
+ assertThat(xml).contains("01020304");
+
+ // Validate structure
+ assertThat(xml).contains("Complex Service with Special & Characters <test>");
+ }
+
+ @Test
+ @DisplayName("Debug: Minimal UsagePoint (mostly nulls)")
+ void debugMinimalUsagePoint() throws Exception {
+ // Create minimal UsagePoint - full constructor has 15 parameters
+ UsagePointDto usagePoint = new UsagePointDto(
+ "urn:uuid:minimal-test", // uuid
+ null, // description
+ null, // roleFlags
+ null, // serviceCategory
+ null, // status
+ null, null, null, null, // 4 measurement fields
+ null, null, null, // serviceDeliveryPoint, pnodeRefs, aggregatedNodeRefs
+ null, null, null // 3 collection fields
+ );
+
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(usagePoint);
+
+ // Print for debugging
+ System.out.println("\nMinimal XML (Jackson 3):");
+ System.out.println("========================");
+ System.out.println(xml);
+ System.out.println("========================\n");
+
+ // Validate minimal structure
+ assertThat(xml).contains("UsagePoint");
+ assertThat(xml).contains("http://naesb.org/espi");
+
+ // Validate that null fields are not included (NON_EMPTY policy)
+ assertThat(xml).doesNotContain(" marshaller.marshal(timeConfig, writer));
-
- String xml = writer.toString();
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(timeConfig);
// Verify XML structure
- assertTrue(xml.contains("TimeConfiguration"), "XML should contain TimeConfiguration element");
- assertTrue(xml.contains("http://naesb.org/espi"), "XML should contain ESPI namespace");
- assertTrue(xml.contains("mRID"), "XML should contain mRID attribute");
- assertTrue(xml.contains("550e8400-e29b-41d4-a716-446655440000"), "XML should contain UUID");
- assertTrue(xml.contains("-28800"), "XML should contain timezone offset");
- assertTrue(xml.contains("3600"), "XML should contain DST offset");
+ assertThat(xml).contains("TimeConfiguration");
+ assertThat(xml).contains("http://naesb.org/espi");
+ assertThat(xml).contains("mRID"); // DTO uses mRID attribute for UUID
+ assertThat(xml).contains("550e8400-e29b-51d4-a716-446655440000"); // Version-5 UUID
+ assertThat(xml).contains("-28800"); // Jackson 3 uses default namespace
+ assertThat(xml).contains("3600");
}
@Test
@DisplayName("Should perform round-trip marshalling for TimeConfigurationDto")
- void shouldPerformRoundTripMarshallingForTimeConfiguration() throws JAXBException {
+ void shouldPerformRoundTripMarshallingForTimeConfiguration() throws Exception {
// Create original TimeConfiguration with Eastern Time (UTC-5)
TimeConfigurationDto original = new TimeConfigurationDto(
null, // id
- "urn:uuid:commercial-time-config", // uuid
+ "urn:uuid:550e8400-e29b-51d4-a716-446655440001", // uuid (Version-5)
new byte[]{0x01, 0x0B, 0x01, 0x00, 0x02, 0x00}, // dstEndRule
3600L, // dstOffset
new byte[]{0x01, 0x03, 0x08, 0x00, 0x02, 0x00}, // dstStartRule
-18000L // tzOffset (UTC-5)
);
- // Marshal to XML
- StringWriter writer = new StringWriter();
- marshaller.marshal(original, writer);
- String xml = writer.toString();
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(original);
- // Unmarshal back from XML
- StringReader reader = new StringReader(xml);
- TimeConfigurationDto roundTrip = (TimeConfigurationDto) unmarshaller.unmarshal(reader);
+ // Unmarshal back from XML using Jackson 3
+ TimeConfigurationDto roundTrip = xmlMapper.readValue(xml, TimeConfigurationDto.class);
// Verify data integrity survived round trip
- assertEquals(original.getUuid(), roundTrip.getUuid(),
- "UUID should survive round trip");
- assertEquals(original.getTzOffset(), roundTrip.getTzOffset(),
- "Timezone offset should survive round trip");
- assertEquals(original.getDstOffset(), roundTrip.getDstOffset(),
- "DST offset should survive round trip");
- assertArrayEquals(original.getDstStartRule(), roundTrip.getDstStartRule(),
- "DST start rule should survive round trip");
- assertArrayEquals(original.getDstEndRule(), roundTrip.getDstEndRule(),
- "DST end rule should survive round trip");
+ assertThat(roundTrip.getUuid()).isEqualTo(original.getUuid());
+ assertThat(roundTrip.getTzOffset()).isEqualTo(original.getTzOffset());
+ assertThat(roundTrip.getDstOffset()).isEqualTo(original.getDstOffset());
+ assertThat(roundTrip.getDstStartRule()).isEqualTo(original.getDstStartRule());
+ assertThat(roundTrip.getDstEndRule()).isEqualTo(original.getDstEndRule());
}
@Test
@DisplayName("Should handle TimeConfigurationDto with only timezone offset")
- void shouldHandleTimeConfigurationWithOnlyTimezoneOffset() throws JAXBException {
+ void shouldHandleTimeConfigurationWithOnlyTimezoneOffset() throws Exception {
// Create TimeConfiguration with only timezone offset (no DST)
TimeConfigurationDto simple = new TimeConfigurationDto(
null, // id
- "urn:uuid:simple-timezone", // uuid
+ "urn:uuid:550e8400-e29b-51d4-a716-446655440002", // uuid (Version-5)
null, // dstEndRule
null, // dstOffset
null, // dstStartRule
7200L // tzOffset (UTC+2)
);
- // Marshal to XML
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(simple, writer));
-
- String xml = writer.toString();
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(simple);
// Verify XML structure
- assertTrue(xml.contains("TimeConfiguration"), "XML should contain TimeConfiguration element");
- assertTrue(xml.contains("7200"), "XML should contain timezone offset");
- assertFalse(xml.contains("dstOffset"), "XML should not contain DST offset element");
- assertFalse(xml.contains("dstStartRule"), "XML should not contain DST start rule element");
- assertFalse(xml.contains("dstEndRule"), "XML should not contain DST end rule element");
+ assertThat(xml).contains("TimeConfiguration");
+ assertThat(xml).contains("7200"); // Jackson 3 uses default namespace
+ assertThat(xml).doesNotContain("dstOffset");
+ assertThat(xml).doesNotContain("dstStartRule");
+ assertThat(xml).doesNotContain("dstEndRule");
- // Unmarshal back
- StringReader reader = new StringReader(xml);
- TimeConfigurationDto roundTrip = (TimeConfigurationDto) unmarshaller.unmarshal(reader);
+ // Unmarshal back using Jackson 3
+ TimeConfigurationDto roundTrip = xmlMapper.readValue(xml, TimeConfigurationDto.class);
// Verify data integrity
- assertEquals(simple.getTzOffset(), roundTrip.getTzOffset(), "Timezone offset should survive round trip");
- assertNull(roundTrip.getDstOffset(), "DST offset should be null");
- assertNull(roundTrip.getDstStartRule(), "DST start rule should be null");
- assertNull(roundTrip.getDstEndRule(), "DST end rule should be null");
+ assertThat(roundTrip.getTzOffset()).isEqualTo(simple.getTzOffset());
+ assertThat(roundTrip.getDstOffset()).isNull();
+ assertThat(roundTrip.getDstStartRule()).isNull();
+ assertThat(roundTrip.getDstEndRule()).isNull();
}
@Test
@DisplayName("Should handle empty TimeConfigurationDto without errors")
- void shouldHandleEmptyTimeConfigurationWithoutErrors() throws JAXBException {
+ void shouldHandleEmptyTimeConfigurationWithoutErrors() throws Exception {
// Create empty TimeConfiguration
TimeConfigurationDto empty = new TimeConfigurationDto();
- // Marshal to XML
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(empty, writer));
-
- String xml = writer.toString();
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(empty);
// Should still contain basic structure
- assertTrue(xml.contains("TimeConfiguration"), "XML should contain TimeConfiguration element");
- assertTrue(xml.contains("http://naesb.org/espi"), "XML should contain ESPI namespace");
+ assertThat(xml).contains("TimeConfiguration");
+ assertThat(xml).contains("http://naesb.org/espi");
- // Unmarshal back
- StringReader reader = new StringReader(xml);
- TimeConfigurationDto roundTrip = (TimeConfigurationDto) unmarshaller.unmarshal(reader);
+ // Unmarshal back using Jackson 3
+ TimeConfigurationDto roundTrip = xmlMapper.readValue(xml, TimeConfigurationDto.class);
// Should not throw exceptions
- assertNotNull(roundTrip, "Round trip should produce valid object");
+ assertThat(roundTrip).isNotNull();
}
@Test
@DisplayName("Should include proper XML namespaces and element order")
- void shouldIncludeProperXmlNamespacesAndElementOrder() {
+ void shouldIncludeProperXmlNamespacesAndElementOrder() throws Exception {
// Create TimeConfiguration with all fields
TimeConfigurationDto timeConfig = new TimeConfigurationDto(
null,
- "urn:uuid:test-namespaces",
+ "urn:uuid:550e8400-e29b-51d4-a716-446655440003", // uuid (Version-5)
new byte[]{0x01}, // dstEndRule
3600L, // dstOffset
new byte[]{0x01}, // dstStartRule
-28800L // tzOffset
);
- // Marshal to XML
- StringWriter writer = new StringWriter();
- assertDoesNotThrow(() -> marshaller.marshal(timeConfig, writer));
- String xml = writer.toString();
+ // Marshal to XML using Jackson 3
+ String xml = xmlMapper.writeValueAsString(timeConfig);
// Verify namespace declarations
- assertTrue(xml.contains("xmlns") && xml.contains("http://naesb.org/espi"),
- "XML should contain ESPI namespace declaration");
+ assertThat(xml).contains("xmlns");
+ assertThat(xml).contains("http://naesb.org/espi");
// Verify element order matches XSD propOrder: dstEndRule, dstOffset, dstStartRule, tzOffset
int dstEndRulePos = xml.indexOf("dstEndRule");
@@ -207,9 +196,9 @@ void shouldIncludeProperXmlNamespacesAndElementOrder() {
int dstStartRulePos = xml.indexOf("dstStartRule");
int tzOffsetPos = xml.indexOf("tzOffset");
- assertTrue(dstEndRulePos < dstOffsetPos, "dstEndRule should come before dstOffset");
- assertTrue(dstOffsetPos < dstStartRulePos, "dstOffset should come before dstStartRule");
- assertTrue(dstStartRulePos < tzOffsetPos, "dstStartRule should come before tzOffset");
+ assertThat(dstEndRulePos).isLessThan(dstOffsetPos);
+ assertThat(dstOffsetPos).isLessThan(dstStartRulePos);
+ assertThat(dstStartRulePos).isLessThan(tzOffsetPos);
}
@Test
diff --git a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImplTest.java b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImplTest.java
index 16fe434f..3b4d0f19 100644
--- a/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImplTest.java
+++ b/openespi-common/src/test/java/org/greenbuttonalliance/espi/common/service/impl/DtoExportServiceImplTest.java
@@ -12,19 +12,25 @@
import org.greenbuttonalliance.espi.common.repositories.usage.UsagePointRepository;
import org.jspecify.annotations.NonNull;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import static org.assertj.core.api.Assertions.assertThat;
+
class DtoExportServiceImplTest {
@@ -45,27 +51,219 @@ void setUp() {
}
@Test
- void export_atom_feed_test() throws IOException {
- LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS);
+ @DisplayName("Should export Atom feed with valid XML structure and metadata")
+ void shouldExportAtomFeedWithValidXmlStructure() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
AtomEntryDto usagePointEntryDto = getUsagePointEntry(now);
-
AtomEntryDto meterReadingEntryDto = getMeeterReadingEntryDto(now);
-
AtomEntryDto readingEntry = getReadingEntryDto(now);
+ AtomEntryDto intervalBlockEntry = getIntervlBlockEntryDto(now);
+
+ AtomFeedDto atomFeedDto = new AtomFeedDto("urn:uuid:15B0A4ED-CCF4-5521-A0A1-9FF650EC8A6B",
+ "Green Button Subscription Feed",
+ now, now, null,
+ List.of(usagePointEntryDto, meterReadingEntryDto, readingEntry, intervalBlockEntry));
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Print for debugging (can be removed later)
+ System.out.println(xml);
+
+ // Assert - XML Declaration
+ assertThat(xml).startsWith("");
+
+ // Assert - Root element
+ assertThat(xml).contains("");
+
+ // Assert - Feed metadata (using Version-5 UUID)
+ assertThat(xml).contains("urn:uuid:15B0A4ED-CCF4-5521-A0A1-9FF650EC8A6B");
+ assertThat(xml).contains("Green Button Subscription Feed");
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - Entry structure
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - ESPI namespace in content
+ assertThat(xml).contains("xmlns:espi=\"http://naesb.org/espi\"");
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - All 4 entries present
+ assertThat(xml).contains("Front Electric Meter"); // UsagePoint title
+ assertThat(xml).contains("Meter Reading"); // MeterReading title
+ assertThat(xml).contains("Type of Meter Reading Data"); // ReadingType title
+ assertThat(xml).contains("Interval Block"); // IntervalBlock title
+ }
+
+ @Test
+ @DisplayName("Should export Atom entries with proper metadata elements")
+ void shouldExportAtomEntriesWithProperMetadata() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:test-feed-id", "Test Feed", now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - Entry has required Atom elements
+ assertThat(xml).contains("");
+ assertThat(xml).containsPattern("urn:uuid:[0-9a-fA-F-]{36}"); // UUID format
+ assertThat(xml).contains(""); // Should have title element
+ assertThat(xml).contains(""); // ISO 8601 timestamp
+ assertThat(xml).contains(""); // ISO 8601 timestamp
+
+ // Assert - Timestamps are ISO 8601 format
+ assertThat(xml).containsPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}");
+ assertThat(xml).containsPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}");
+ }
+
+ @Test
+ @DisplayName("Should export ESPI UsagePoint content correctly")
+ void shouldExportEspiUsagePointContent() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:test-feed", "Test Feed", now, now, null,
+ List.of(usagePointEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - ESPI namespace
+ assertThat(xml).contains("xmlns:espi=\"http://naesb.org/espi\"");
+
+ // Assert - UsagePoint element (with namespace declaration)
+ assertThat(xml).contains("");
+
+ // Assert - ServiceCategory field
+ assertThat(xml).contains("ServiceCategory");
+ assertThat(xml).contains("ELECTRICITY");
+ }
+ @Test
+ @DisplayName("Should use Version-5 UUIDs in test data")
+ void shouldUseVersion5UuidsInTestData() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto usagePointEntry = getUsagePointEntry(now);
+ AtomEntryDto readingEntry = getReadingEntryDto(now);
AtomEntryDto intervalBlockEntry = getIntervlBlockEntryDto(now);
- AtomFeedDto atomFeedDto = new AtomFeedDto("urn:uuid:15B0A4ED-CCF4-4521-A0A1-9FF650EC8A6B", "Green Button Subscription Feed",
- now, now, null, List.of(usagePointEntryDto, meterReadingEntryDto, readingEntry, intervalBlockEntry));
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:15B0A4ED-CCF4-5521-A0A1-9FF650EC8A6B", // Version-5
+ "Test Feed", now, now, null,
+ List.of(usagePointEntry, readingEntry, intervalBlockEntry)
+ );
- try (OutputStream stream = new ByteArrayOutputStream()) {
- // Commented out due to conflict in IntervalReadingDto which cannot be fixed in this task
- dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
- System.out.println(stream.toString());
- }
+ // Assert - Feed ID uses Version-5 UUID (note '5' in version field)
+ assertThat(xml).contains("urn:uuid:15B0A4ED-CCF4-5521-A0A1-9FF650EC8A6B");
+
+ // Assert - Entry IDs use Version-5 UUIDs
+ assertThat(xml).contains("urn:uuid:48C2A019-5598-5E16-B0F9-49E4FF27F5FB"); // UsagePoint
+ assertThat(xml).contains("urn:uuid:3430B025-65D5-593A-BEC2-053603C91CD7"); // ReadingType
+ assertThat(xml).contains("urn:uuid:FE9A61BB-6913-52D4-88BE-9634A218EF53"); // IntervalBlock
+
+ // Assert - Version field should be '5' (3rd group, 1st char)
+ // Format: xxxxxxxx-xxxx-5xxx-xxxx-xxxxxxxxxxxx
+ assertThat(xml).containsPattern("urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
+ }
+
+ @Test
+ @DisplayName("Should export ESPI ReadingType content correctly")
+ void shouldExportEspiReadingTypeContent() throws IOException {
+ // Arrange
+ LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
+ OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
+
+ AtomEntryDto readingEntry = getReadingEntryDto(now);
+ AtomFeedDto atomFeedDto = new AtomFeedDto(
+ "urn:uuid:test-feed", "Test Feed", now, now, null,
+ List.of(readingEntry)
+ );
+
+ // Act
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ dtoExportService.exportAtomFeed(atomFeedDto, stream);
+ String xml = stream.toString(StandardCharsets.UTF_8);
+
+ // Assert - ReadingType element (may have attributes)
+ assertThat(xml).contains("");
+
+ // Assert - ReadingType fields
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - IntervalBlock interval
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - IntervalReading elements
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+ assertThat(xml).contains("");
+
+ // Assert - Multiple readings present (4 readings in test data)
+ int readingCount = xml.split(" intervalBlockLinks = new ArrayList<>();
intervalBlockLinks.add(new LinkDto("self", "/espi/1_1/resource/RetailCustomer/9B6C7066/UsagePoint/5446AF3F/MeterReading/01/IntervalBlock/173"));
intervalBlockLinks.add(new LinkDto("up", "/espi/1_1/resource/RetailCustomer/9B6C7066/UsagePoint/5446AF3F/MeterReading/01/IntervalBlock"));
- return new AtomEntryDto("urn:uuid:FE9A61BB-6913-42D4-88BE-9634A218EF53", "Interval Block", now, now,
+ return new AtomEntryDto("urn:uuid:FE9A61BB-6913-52D4-88BE-9634A218EF53", "Interval Block", now, now,
intervalBlockLinks, intervalBlockDto);
}
private static @NonNull AtomEntryDto getReadingEntryDto(OffsetDateTime now) {
- ReadingTypeDto readingTypeDto = new ReadingTypeDto(1L, "urn:uuid:3430B025-65D5-493A-BEC2-053603C91CD7",
+ ReadingTypeDto readingTypeDto = new ReadingTypeDto(1L, "urn:uuid:3430B025-65D5-593A-BEC2-053603C91CD7",
null, "4", "1", null, "840", "12", "NET", "TOTAL", 900L, "NET", "KILO", "DAILY", "V", "1", "CONTINUOUS", "1", null,
null, null);
@@ -98,7 +296,7 @@ void export_atom_feed_test() throws IOException {
readingTypeLinkList.add(new LinkDto("self", "/espi/1_1/resource/ReadingType/07"));
readingTypeLinkList.add(new LinkDto("up", "/espi/1_1/resource/ReadingType"));
- return new AtomEntryDto("urn:uuid:3430B025-65D5-493A-BEC2-053603C91CD7", "Type of Meter Reading Data", now, now,
+ return new AtomEntryDto("urn:uuid:3430B025-65D5-593A-BEC2-053603C91CD7", "Type of Meter Reading Data", now, now,
readingTypeLinkList, readingTypeDto);
}
@@ -115,7 +313,7 @@ void export_atom_feed_test() throws IOException {
AtomEntryDto getUsagePointEntry(OffsetDateTime now) {
UsagePointEntity usagePointEntity = new UsagePointEntity();
- usagePointEntity.setId(UUID.fromString("48C2A019-5598-4E16-B0F9-49E4FF27F5FB"));
+ usagePointEntity.setId(UUID.fromString("48C2A019-5598-5E16-B0F9-49E4FF27F5FB"));
usagePointEntity.setSelfLink(new LinkType("self", "/espi/1_1/resource/RetailCustomer/9B6C7066/UsagePoint/5446AF3F"));
usagePointEntity.setUpLink(new LinkType("up", "/espi/1_1/resource/RetailCustomer/9B6C7066/UsagePoint"));
List relatedLinks = new ArrayList<>();
@@ -139,7 +337,7 @@ AtomEntryDto getUsagePointEntry(OffsetDateTime now) {
UsagePointDto usagePointDto = usagePointMapper.toDto(usagePointEntity);
- return new AtomEntryDto("urn:uuid:48C2A019-5598-4E16-B0F9-49E4FF27F5FB", "Front Electric Meter",
+ return new AtomEntryDto("urn:uuid:48C2A019-5598-5E16-B0F9-49E4FF27F5FB", "Front Electric Meter",
now,
now,
usagePointList,