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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions