Skip to content

Refactor TimeConfigurationDto and UsagePointDto from POJOs to records #61

@dfcoffin

Description

@dfcoffin

Summary

Convert TimeConfigurationDto and UsagePointDto from POJO classes to records to match the pattern used by 94% of DTOs in the codebase (34 out of 36 DTOs are records).

Current State

DTO Class Structure (36 total):

  • 34 DTOs are records (94%) - All other DTOs use the record pattern
  • 2 DTOs are POJOs (6%) - TimeConfigurationDto, UsagePointDto

Problem

Both POJO DTOs use @XmlAccessorType(XmlAccessType.PROPERTY) which causes Jackson 3 to serialize ALL public getters, including utility methods:

TimeConfigurationDto - Utility methods being serialized:

@XmlAccessorType(XmlAccessType.PROPERTY)  // ❌ Serializes ALL public getters
public class TimeConfigurationDto {
    // Data fields...
    
    // These utility methods get serialized into XML (incorrect):
    public Double getTzOffsetInHours() { ... }
    public Double getDstOffsetInHours() { ... }
    public Long getEffectiveOffset() { ... }
    public Double getEffectiveOffsetInHours() { ... }
    public boolean hasDstRules() { ... }
    public boolean isDstActive() { ... }
}

Current workaround: Add @XmlTransient to every utility method getter

Solution

Convert both DTOs to records following the ReadingTypeDto pattern:

ReadingTypeDto pattern (correct approach):

@XmlRootElement(name = "ReadingType", namespace = "http://naesb.org/espi")
@XmlAccessorType(XmlAccessType.FIELD)  // ✅ Serializes only record components
@XmlType(name = "ReadingType", namespace = "http://naesb.org/espi", propOrder = {...})
public record ReadingTypeDto(
    @XmlTransient Long id,
    @XmlTransient String uuid,
    @XmlElement(name = "description") String description,
    @XmlElement(name = "commodity") String commodity,
    // ... other fields with JAXB annotations on record components
) {
    // No-arg constructor for JAXB
    public ReadingTypeDto() {
        this(null, null, null, null, ...);
    }
    
    // Convenience constructors (all delegate to canonical constructor)
    public ReadingTypeDto(Long id, String uuid, String description) {
        this(id, uuid, description, null, null, ...);
    }
    
    // Utility methods (no @XmlTransient needed with FIELD access)
    public boolean isEnergyMeasurement() { ... }
    public Long getIntervalLengthInMinutes() { ... }
    public String getReadingTypeSummary() { ... }
}

Changes Required

1. TimeConfigurationDto Refactoring

File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/TimeConfigurationDto.java

Changes:

  1. Convert public class to public record
  2. Change @XmlAccessorType(XmlAccessType.PROPERTY) to @XmlAccessorType(XmlAccessType.FIELD)
  3. Move JAXB annotations from getters to record component parameters
  4. Add @XmlTransient to id and uuid components
  5. Convert all constructors to delegate to canonical constructor
  6. Keep utility methods as-is (no @XmlTransient needed)

Before:

@XmlAccessorType(XmlAccessType.PROPERTY)
public class TimeConfigurationDto {
    private Long id;
    private String uuid;
    private byte[] dstEndRule;
    private Long dstOffset;
    private byte[] dstStartRule;
    private Long tzOffset;
    
    @XmlTransient
    public Long getId() { return id; }
    
    @XmlElement(name = "dstEndRule")
    public byte[] getDstEndRule() { return dstEndRule != null ? dstEndRule.clone() : null; }
    
    // ... other getters/setters
    
    // Utility methods (need @XmlTransient with PROPERTY access)
    public Double getTzOffsetInHours() { ... }
}

After:

@XmlAccessorType(XmlAccessType.FIELD)
public record TimeConfigurationDto(
    @XmlTransient
    Long id,
    
    @XmlTransient
    String uuid,
    
    @XmlElement(name = "dstEndRule", type = String.class)
    @XmlJavaTypeAdapter(HexBinaryAdapter.class)
    byte[] dstEndRule,
    
    @XmlElement(name = "dstOffset")
    Long dstOffset,
    
    @XmlElement(name = "dstStartRule", type = String.class)
    @XmlJavaTypeAdapter(HexBinaryAdapter.class)
    byte[] dstStartRule,
    
    @XmlElement(name = "tzOffset")
    Long tzOffset
) {
    // No-arg constructor for JAXB
    public TimeConfigurationDto() {
        this(null, null, null, null, null, null);
    }
    
    // Convenience constructors
    public TimeConfigurationDto(Long tzOffset) {
        this(null, null, null, null, null, tzOffset);
    }
    
    public TimeConfigurationDto(String uuid, Long tzOffset) {
        this(null, uuid, null, null, null, tzOffset);
    }
    
    // Utility methods (no @XmlTransient needed with FIELD access)
    public Double getTzOffsetInHours() {
        return tzOffset != null ? tzOffset / 3600.0 : null;
    }
    
    public Double getDstOffsetInHours() {
        return dstOffset != null ? dstOffset / 3600.0 : null;
    }
    
    public Long getEffectiveOffset() {
        return tzOffset != null ? tzOffset + (dstOffset != null ? dstOffset : 0L) : null;
    }
    
    public Double getEffectiveOffsetInHours() {
        Long effective = getEffectiveOffset();
        return effective != null ? effective / 3600.0 : null;
    }
    
    public boolean hasDstRules() {
        return dstStartRule != null && dstStartRule.length > 0 
            && dstEndRule != null && dstEndRule.length > 0;
    }
    
    public boolean isDstActive() {
        return dstOffset != null && dstOffset != 0L;
    }
    
    // Override getters for byte array cloning
    @Override
    public byte[] dstEndRule() {
        return dstEndRule != null ? dstEndRule.clone() : null;
    }
    
    @Override
    public byte[] dstStartRule() {
        return dstStartRule != null ? dstStartRule.clone() : null;
    }
}

2. UsagePointDto Refactoring

File: openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/UsagePointDto.java

Changes:

  1. Same pattern as TimeConfigurationDto
  2. Convert public class to public record
  3. Change @XmlAccessorType(XmlAccessType.PROPERTY) to @XmlAccessorType(XmlAccessType.FIELD)
  4. Move JAXB annotations to record components
  5. Convert 4 existing constructors to delegate to canonical constructor
  6. Keep utility methods: generateSelfHref(), generateUpHref(), getMeterReadingCount(), getUsageSummaryCount()

3. Test Updates

Both DTOs have existing tests that need verification after refactoring:

TimeConfigurationDto:

  • TimeConfigurationDtoTest.java - 11 tests (currently being converted to Jackson 3)

UsagePointDto:

  • Check for existing tests and update if needed
  • Ensure MapStruct mappers still work correctly

4. Mapper Compatibility

Verify MapStruct mappers handle record DTOs:

  • TimeConfigurationMapper
  • UsagePointMapper
  • MapStruct 1.6.0 supports records

Benefits

  1. Consistency - 100% of DTOs will use record pattern (currently 94%)
  2. Immutability - Records are immutable by default
  3. Less boilerplate - No need for explicit getters/setters/equals/hashCode
  4. Type safety - Records provide better compile-time guarantees
  5. Cleaner XML serialization - FIELD access prevents utility methods from being serialized

Testing Requirements

  1. ✅ All existing TimeConfigurationDtoTest tests pass (11 tests)
  2. ✅ All UsagePointDto tests pass
  3. ✅ MapStruct mappers compile and work correctly
  4. ✅ Jackson 3 XML marshalling/unmarshalling works correctly
  5. ✅ Round-trip serialization preserves data integrity
  6. ✅ Utility methods return correct values
  7. ✅ No regression in integration tests

Dependencies

Related Issues:

  • #XX - Jackson 3 XML Marshalling Test Plan (covers test updates)

Related Files:

  • JACKSON3_XML_MARSHALLING_TEST_PLAN.md - Documents Jackson 3 test conversion
  • ReadingTypeDto.java - Template/reference for record pattern

Implementation Checklist

  • Convert TimeConfigurationDto to record
    • Change class to record with annotated components
    • Update constructors
    • Keep utility methods
    • Handle byte array cloning in overridden getters
  • Convert UsagePointDto to record
    • Change class to record with annotated components
    • Update 4 constructors
    • Keep utility methods
  • Verify TimeConfigurationDtoTest passes (11 tests)
  • Verify UsagePointDto tests pass
  • Verify MapStruct mappers compile
  • Run full test suite
  • Update documentation if needed

Success Criteria

  1. ✅ TimeConfigurationDto is a record with FIELD access
  2. ✅ UsagePointDto is a record with FIELD access
  3. ✅ All tests pass
  4. ✅ Utility methods work correctly without @XmlTransient
  5. ✅ Jackson 3 marshalling produces clean XML (no utility method output)
  6. ✅ MapStruct mappers work correctly
  7. ✅ 100% of DTOs use record pattern (36/36)

Reference

Record DTO Template (ReadingTypeDto):
openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/usage/ReadingTypeDto.java

Key Pattern:

  • @XmlAccessorType(XmlAccessType.FIELD) on record
  • JAXB annotations on record component parameters
  • No-arg constructor for JAXB compatibility
  • Convenience constructors delegate to canonical constructor
  • Utility methods don't need @XmlTransient

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions