Skip to content

Commit e48ab3e

Browse files
dfcoffinclaude
andauthored
refactor: Convert TimeConfigurationDto and UsagePointDto to records with FIELD access (#65)
Completes Issue #61 - Convert remaining DTO POJOs to Java records for improved immutability and cleaner code. Changes: - Converted TimeConfigurationDto from POJO class to record with @XmlAccessType(FIELD) - Converted UsagePointDto from POJO class to record with @XmlAccessType(FIELD) - Moved JAXB annotations from getter methods to record component parameters - Added defensive byte array cloning in overridden accessors - Eliminated @XmlTransient annotations on utility methods (no longer needed with FIELD access) - Updated AtomEntryDto to dynamically add xmlns:espi and xmlns:cust namespace declarations - Enhanced @JsonSubTypes to include all 17 ESPI resources (9 usage + 8 customer) - Updated test assertions to accommodate namespace attributes in entry elements Benefits: - All 36 DTOs now use consistent record pattern (100% adoption) - Reduced code from 355 lines by eliminating boilerplate - Simplified namespace handling with auto-computed attributes - Maintains full Jackson 3 XmlMapper compatibility - All 554 tests passing with no regressions Testing: - TimeConfigurationDtoTest: 11/11 tests passing - Jackson3XmlMarshallingTest: 7/7 tests passing - DtoExportServiceImplTest: 6/6 tests passing - Full test suite: 554/554 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent ca41115 commit e48ab3e

File tree

6 files changed

+214
-355
lines changed

6 files changed

+214
-355
lines changed

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/dto/atom/AtomEntryDto.java

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@
2323
import com.fasterxml.jackson.annotation.JsonSubTypes;
2424
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2525
import jakarta.xml.bind.annotation.*;
26-
import org.greenbuttonalliance.espi.common.dto.usage.MeterReadingDto;
27-
import org.greenbuttonalliance.espi.common.dto.usage.ReadingTypeDto;
28-
import org.greenbuttonalliance.espi.common.dto.usage.UsagePointDto;
26+
import org.greenbuttonalliance.espi.common.dto.customer.*;
27+
import org.greenbuttonalliance.espi.common.dto.usage.*;
2928

3029
import java.time.LocalDateTime;
3130
import java.time.OffsetDateTime;
@@ -44,53 +43,93 @@
4443
"id", "title", "published", "updated", "links", "content"
4544
})
4645
public record AtomEntryDto(
47-
46+
4847
@XmlElement(name = "id", namespace = "http://www.w3.org/2005/Atom")
4948
String id,
50-
49+
5150
@XmlElement(name = "title", namespace = "http://www.w3.org/2005/Atom")
5251
String title,
5352

5453
@XmlElement(name = "published", namespace = "http://www.w3.org/2005/Atom")
5554
OffsetDateTime published,
56-
55+
5756
@XmlElement(name = "updated", namespace = "http://www.w3.org/2005/Atom")
5857
OffsetDateTime updated,
59-
58+
6059
@XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
6160
List<LinkDto> links,
6261

6362
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
6463
include = JsonTypeInfo.As.WRAPPER_OBJECT,
6564
property = "type")
6665
@JsonSubTypes({
67-
@JsonSubTypes.Type(value = UsagePointDto.class, name = "espi:UsagePoint"),
66+
// ESPI Usage resources (espi: namespace - top-level IdentifiedObject-based)
67+
@JsonSubTypes.Type(value = ApplicationInformationDto.class, name = "espi:ApplicationInformation"),
68+
@JsonSubTypes.Type(value = AuthorizationDto.class, name = "espi:Authorization"),
69+
@JsonSubTypes.Type(value = ElectricPowerQualitySummaryDto.class, name = "espi:ElectricPowerQualitySummary"),
70+
@JsonSubTypes.Type(value = IntervalBlockDto.class, name = "espi:IntervalBlock"),
6871
@JsonSubTypes.Type(value = MeterReadingDto.class, name = "espi:MeterReading"),
69-
@JsonSubTypes.Type(value = ReadingTypeDto.class, name = "espi:ReadingType")
72+
@JsonSubTypes.Type(value = ReadingTypeDto.class, name = "espi:ReadingType"),
73+
@JsonSubTypes.Type(value = TimeConfigurationDto.class, name = "espi:TimeConfiguration"),
74+
@JsonSubTypes.Type(value = UsagePointDto.class, name = "espi:UsagePoint"),
75+
@JsonSubTypes.Type(value = UsageSummaryDto.class, name = "espi:UsageSummary"),
76+
// ESPI Customer resources (cust: namespace - top-level IdentifiedObject-based)
77+
@JsonSubTypes.Type(value = CustomerDto.class, name = "cust:Customer"),
78+
@JsonSubTypes.Type(value = CustomerAccountDto.class, name = "cust:CustomerAccount"),
79+
@JsonSubTypes.Type(value = CustomerAgreementDto.class, name = "cust:CustomerAgreement"),
80+
@JsonSubTypes.Type(value = EndDeviceDto.class, name = "cust:EndDevice"),
81+
@JsonSubTypes.Type(value = MeterDto.class, name = "cust:Meter"),
82+
@JsonSubTypes.Type(value = ProgramDateIdMappingsDto.class, name = "cust:ProgramDateIdMappings"),
83+
// Note: TimeConfigurationDto supports BOTH espi: and cust: namespaces (same type in both schemas)
84+
@JsonSubTypes.Type(value = TimeConfigurationDto.class, name = "cust:TimeConfiguration"),
85+
@JsonSubTypes.Type(value = ServiceLocationDto.class, name = "cust:ServiceLocation"),
86+
@JsonSubTypes.Type(value = StatementDto.class, name = "cust:Statement")
87+
// TODO: Add when ServiceSupplierDto is implemented:
88+
// @JsonSubTypes.Type(value = ServiceSupplierDto.class, name = "cust:ServiceSupplier")
7089
})
7190
@XmlAnyElement(lax = true)
7291
@XmlElement(name = "content", namespace = "http://www.w3.org/2005/Atom")
73-
Object content
92+
Object content,
93+
94+
@XmlAttribute(name = "xmlns:espi")
95+
String espiNamespace,
96+
97+
@XmlAttribute(name = "xmlns:cust")
98+
String custNamespace
7499
) {
75-
100+
76101
/**
77-
* Default constructor for JAXB.
102+
* Compact constructor that auto-computes namespace attributes based on content type.
78103
*/
79-
public AtomEntryDto() {
80-
this(null, null, null, null, null, null);
104+
public AtomEntryDto {
105+
// Auto-compute namespaces if not provided
106+
if (content != null && espiNamespace == null && custNamespace == null) {
107+
String packageName = content.getClass().getPackageName();
108+
if (packageName.contains("dto.usage")) {
109+
espiNamespace = "http://naesb.org/espi";
110+
} else if (packageName.contains("dto.customer")) {
111+
custNamespace = "http://naesb.org/espi/customer";
112+
}
113+
}
81114
}
82-
115+
83116
/**
84-
* Constructor for basic entry data.
117+
* Constructor for basic entry data without namespace (auto-computed).
85118
*/
86-
public AtomEntryDto(String id, String title, Object resource) {
119+
public AtomEntryDto(String id, String title, OffsetDateTime published,
120+
OffsetDateTime updated, List<LinkDto> links, Object content) {
121+
this(id, title, published, updated, links, content, null, null);
122+
}
87123

88-
//get date in UTC and truncate to seconds for proper ESPI date format
124+
/**
125+
* Constructor for basic entry data with auto-generated timestamps.
126+
*/
127+
public AtomEntryDto(String id, String title, Object resource) {
89128
LocalDateTime localDateTime = LocalDateTime.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS);
90129
OffsetDateTime now = localDateTime.atOffset(ZoneOffset.UTC).toZonedDateTime().toOffsetDateTime();
91130

92131
this(id, title, now, now, null,
93-
new AtomContentDto("application/xml", resource));
132+
new AtomContentDto("application/xml", resource), null, null);
94133
}
95134

96135
/**

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

Lines changed: 47 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@
2121

2222
import io.swagger.v3.oas.annotations.media.Schema;
2323
import jakarta.xml.bind.annotation.*;
24+
import jakarta.xml.bind.annotation.adapters.HexBinaryAdapter;
25+
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
2426

2527
/**
26-
* TimeConfiguration DTO class for JAXB XML marshalling/unmarshalling.
27-
*
28-
* PROTOTYPE: Traditional approach using mutable class with JAXB.
29-
* Alternative to Jackson XML record-based TimeConfigurationDtoJackson.
28+
* TimeConfiguration DTO record for JAXB XML marshalling/unmarshalling.
3029
*
3130
* Represents time configuration parameters including timezone offset and
3231
* daylight saving time rules for energy metering systems.
@@ -37,26 +36,48 @@
3736
* @see <a href="https://www.naesb.org/ESPI_Standards.asp">NAESB ESPI 4.0</a>
3837
*/
3938
@XmlRootElement(name = "TimeConfiguration", namespace = "http://naesb.org/espi")
40-
@XmlAccessorType(XmlAccessType.PROPERTY)
39+
@XmlAccessorType(XmlAccessType.FIELD)
4140
@XmlType(name = "TimeConfiguration", namespace = "http://naesb.org/espi", propOrder = {
4241
"dstEndRule",
4342
"dstOffset",
4443
"dstStartRule",
4544
"tzOffset"
4645
})
47-
public class TimeConfigurationDto {
46+
public record TimeConfigurationDto(
47+
48+
@XmlTransient
49+
@Schema(description = "Internal DTO identifier (not serialized to XML)")
50+
Long id,
51+
52+
@XmlTransient
53+
@Schema(description = "Resource identifier (mRID)", example = "550e8400-e29b-41d4-a716-446655440000")
54+
String uuid,
55+
56+
@XmlElement(name = "dstEndRule", type = String.class)
57+
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
58+
@Schema(description = "Rule to calculate end of daylight savings time in the current year. Result of dstEndRule must be greater than result of dstStartRule.", example = "...")
59+
byte[] dstEndRule,
60+
61+
@XmlElement(name = "dstOffset")
62+
@Schema(description = "Daylight savings time offset from local standard time in seconds", example = "3600")
63+
Long dstOffset,
64+
65+
@XmlElement(name = "dstStartRule", type = String.class)
66+
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
67+
@Schema(description = "Rule to calculate start of daylight savings time in the current year. Result of dstEndRule must be greater than result of dstStartRule.", example = "...")
68+
byte[] dstStartRule,
69+
70+
@XmlElement(name = "tzOffset")
71+
@Schema(description = "Local time zone offset from UTC in seconds. Does not include any daylight savings time offsets. Positive values are east of UTC, negative values are west of UTC.", example = "-28800")
72+
Long tzOffset
4873

49-
private Long id;
50-
private String uuid;
51-
private byte[] dstEndRule;
52-
private Long dstOffset;
53-
private byte[] dstStartRule;
54-
private Long tzOffset;
74+
) {
5575

5676
/**
5777
* Default constructor for JAXB.
5878
*/
5979
public TimeConfigurationDto() {
80+
this(null, null, null, null, null, null);
6081
}
6182

6283
/**
@@ -65,7 +86,7 @@ public TimeConfigurationDto() {
6586
* @param tzOffset the timezone offset in seconds from UTC
6687
*/
6788
public TimeConfigurationDto(Long tzOffset) {
68-
this.tzOffset = tzOffset;
89+
this(null, null, null, null, null, tzOffset);
6990
}
7091

7192
/**
@@ -75,104 +96,36 @@ public TimeConfigurationDto(Long tzOffset) {
7596
* @param tzOffset the timezone offset in seconds from UTC
7697
*/
7798
public TimeConfigurationDto(String uuid, Long tzOffset) {
78-
this.uuid = uuid;
79-
this.tzOffset = tzOffset;
99+
this(null, uuid, null, null, null, tzOffset);
80100
}
81101

82102
/**
83-
* Full constructor with all fields.
103+
* Override dstEndRule getter to return cloned array for defensive copying.
84104
*
85-
* @param id internal DTO id
86-
* @param uuid the resource identifier
87-
* @param dstEndRule DST end rule bytes
88-
* @param dstOffset DST offset in seconds
89-
* @param dstStartRule DST start rule bytes
90-
* @param tzOffset timezone offset in seconds from UTC
105+
* @return cloned byte array or null
91106
*/
92-
public TimeConfigurationDto(Long id, String uuid, byte[] dstEndRule, Long dstOffset,
93-
byte[] dstStartRule, Long tzOffset) {
94-
this.id = id;
95-
this.uuid = uuid;
96-
this.dstEndRule = dstEndRule != null ? dstEndRule.clone() : null;
97-
this.dstOffset = dstOffset;
98-
this.dstStartRule = dstStartRule != null ? dstStartRule.clone() : null;
99-
this.tzOffset = tzOffset;
100-
}
101-
102-
// Getters with JAXB annotations
103-
104-
@XmlTransient
105-
@Schema(description = "Internal DTO identifier (not serialized to XML)")
106-
public Long getId() {
107-
return id;
108-
}
109-
110-
@XmlAttribute(name = "mRID")
111-
@Schema(description = "Resource identifier (mRID)", example = "550e8400-e29b-41d4-a716-446655440000")
112-
public String getUuid() {
113-
return uuid;
114-
}
115-
116-
@XmlElement(name = "dstEndRule", namespace = "http://naesb.org/espi")
117-
@Schema(description = "Rule to calculate end of daylight savings time in the current year. Result of dstEndRule must be greater than result of dstStartRule.", example = "...")
118-
public byte[] getDstEndRule() {
107+
@Override
108+
public byte[] dstEndRule() {
119109
return dstEndRule != null ? dstEndRule.clone() : null;
120110
}
121111

122-
@XmlElement(name = "dstOffset", namespace = "http://naesb.org/espi")
123-
@Schema(description = "Daylight savings time offset from local standard time in seconds", example = "3600")
124-
public Long getDstOffset() {
125-
return dstOffset;
126-
}
127-
128-
@XmlElement(name = "dstStartRule", namespace = "http://naesb.org/espi")
129-
@Schema(description = "Rule to calculate start of daylight savings time in the current year. Result of dstEndRule must be greater than result of dstStartRule.", example = "...")
130-
public byte[] getDstStartRule() {
112+
/**
113+
* Override dstStartRule getter to return cloned array for defensive copying.
114+
*
115+
* @return cloned byte array or null
116+
*/
117+
@Override
118+
public byte[] dstStartRule() {
131119
return dstStartRule != null ? dstStartRule.clone() : null;
132120
}
133121

134-
@XmlElement(name = "tzOffset", namespace = "http://naesb.org/espi")
135-
@Schema(description = "Local time zone offset from UTC in seconds. Does not include any daylight savings time offsets. Positive values are east of UTC, negative values are west of UTC.", example = "-28800")
136-
public Long getTzOffset() {
137-
return tzOffset;
138-
}
139-
140-
// Setters for JAXB unmarshalling
141-
142-
public void setId(Long id) {
143-
this.id = id;
144-
}
145-
146-
public void setUuid(String uuid) {
147-
this.uuid = uuid;
148-
}
149-
150-
public void setDstEndRule(byte[] dstEndRule) {
151-
this.dstEndRule = dstEndRule != null ? dstEndRule.clone() : null;
152-
}
153-
154-
public void setDstOffset(Long dstOffset) {
155-
this.dstOffset = dstOffset;
156-
}
157-
158-
public void setDstStartRule(byte[] dstStartRule) {
159-
this.dstStartRule = dstStartRule != null ? dstStartRule.clone() : null;
160-
}
161-
162-
public void setTzOffset(Long tzOffset) {
163-
this.tzOffset = tzOffset;
164-
}
165-
166-
// Utility methods
167-
// TEMPORARY: @XmlTransient annotations prevent serialization with XmlAccessType.PROPERTY
168-
// TODO: Remove when converting to record with XmlAccessType.FIELD (see issue #61)
122+
// Utility methods (no @XmlTransient needed with FIELD access)
169123

170124
/**
171125
* Gets the timezone offset in hours.
172126
*
173127
* @return timezone offset in hours, or null if not set
174128
*/
175-
@XmlTransient
176129
public Double getTzOffsetInHours() {
177130
return tzOffset != null ? tzOffset / 3600.0 : null;
178131
}
@@ -182,7 +135,6 @@ public Double getTzOffsetInHours() {
182135
*
183136
* @return DST offset in hours, or null if not set
184137
*/
185-
@XmlTransient
186138
public Double getDstOffsetInHours() {
187139
return dstOffset != null ? dstOffset / 3600.0 : null;
188140
}
@@ -192,7 +144,6 @@ public Double getDstOffsetInHours() {
192144
*
193145
* @return total offset in seconds including DST
194146
*/
195-
@XmlTransient
196147
public Long getEffectiveOffset() {
197148
Long base = tzOffset != null ? tzOffset : 0L;
198149
Long dst = dstOffset != null ? dstOffset : 0L;
@@ -204,7 +155,6 @@ public Long getEffectiveOffset() {
204155
*
205156
* @return total offset in hours including DST
206157
*/
207-
@XmlTransient
208158
public Double getEffectiveOffsetInHours() {
209159
return getEffectiveOffset() / 3600.0;
210160
}
@@ -214,7 +164,6 @@ public Double getEffectiveOffsetInHours() {
214164
*
215165
* @return true if DST rules are present, false otherwise
216166
*/
217-
@XmlTransient
218167
public boolean hasDstRules() {
219168
return dstStartRule != null && dstStartRule.length > 0 &&
220169
dstEndRule != null && dstEndRule.length > 0;
@@ -225,7 +174,6 @@ public boolean hasDstRules() {
225174
*
226175
* @return true if DST offset is defined and non-zero, false otherwise
227176
*/
228-
@XmlTransient
229177
public boolean isDstActive() {
230178
return dstOffset != null && dstOffset != 0;
231179
}

0 commit comments

Comments
 (0)