-
Notifications
You must be signed in to change notification settings - Fork 1
Description
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:
- Convert
public classtopublic record - Change
@XmlAccessorType(XmlAccessType.PROPERTY)to@XmlAccessorType(XmlAccessType.FIELD) - Move JAXB annotations from getters to record component parameters
- Add
@XmlTransienttoidanduuidcomponents - Convert all constructors to delegate to canonical constructor
- Keep utility methods as-is (no
@XmlTransientneeded)
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:
- Same pattern as TimeConfigurationDto
- Convert
public classtopublic record - Change
@XmlAccessorType(XmlAccessType.PROPERTY)to@XmlAccessorType(XmlAccessType.FIELD) - Move JAXB annotations to record components
- Convert 4 existing constructors to delegate to canonical constructor
- 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
- Consistency - 100% of DTOs will use record pattern (currently 94%)
- Immutability - Records are immutable by default
- Less boilerplate - No need for explicit getters/setters/equals/hashCode
- Type safety - Records provide better compile-time guarantees
- Cleaner XML serialization - FIELD access prevents utility methods from being serialized
Testing Requirements
- ✅ All existing TimeConfigurationDtoTest tests pass (11 tests)
- ✅ All UsagePointDto tests pass
- ✅ MapStruct mappers compile and work correctly
- ✅ Jackson 3 XML marshalling/unmarshalling works correctly
- ✅ Round-trip serialization preserves data integrity
- ✅ Utility methods return correct values
- ✅ 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 conversionReadingTypeDto.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
- ✅ TimeConfigurationDto is a record with FIELD access
- ✅ UsagePointDto is a record with FIELD access
- ✅ All tests pass
- ✅ Utility methods work correctly without
@XmlTransient - ✅ Jackson 3 marshalling produces clean XML (no utility method output)
- ✅ MapStruct mappers work correctly
- ✅ 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