Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.xml.bind.annotation.*;
import org.greenbuttonalliance.espi.common.dto.usage.MeterReadingDto;
import org.greenbuttonalliance.espi.common.dto.usage.ReadingTypeDto;
import org.greenbuttonalliance.espi.common.dto.usage.UsagePointDto;
import org.greenbuttonalliance.espi.common.dto.customer.*;
import org.greenbuttonalliance.espi.common.dto.usage.*;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
Expand All @@ -44,53 +43,93 @@
"id", "title", "published", "updated", "links", "content"
})
public record AtomEntryDto(

@XmlElement(name = "id", namespace = "http://www.w3.org/2005/Atom")
String id,

@XmlElement(name = "title", namespace = "http://www.w3.org/2005/Atom")
String title,

@XmlElement(name = "published", namespace = "http://www.w3.org/2005/Atom")
OffsetDateTime published,

@XmlElement(name = "updated", namespace = "http://www.w3.org/2005/Atom")
OffsetDateTime updated,

@XmlElement(name = "link", namespace = "http://www.w3.org/2005/Atom")
List<LinkDto> links,

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.WRAPPER_OBJECT,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = UsagePointDto.class, name = "espi:UsagePoint"),
// ESPI Usage resources (espi: namespace - top-level IdentifiedObject-based)
@JsonSubTypes.Type(value = ApplicationInformationDto.class, name = "espi:ApplicationInformation"),
@JsonSubTypes.Type(value = AuthorizationDto.class, name = "espi:Authorization"),
@JsonSubTypes.Type(value = ElectricPowerQualitySummaryDto.class, name = "espi:ElectricPowerQualitySummary"),
@JsonSubTypes.Type(value = IntervalBlockDto.class, name = "espi:IntervalBlock"),
@JsonSubTypes.Type(value = MeterReadingDto.class, name = "espi:MeterReading"),
@JsonSubTypes.Type(value = ReadingTypeDto.class, name = "espi:ReadingType")
@JsonSubTypes.Type(value = ReadingTypeDto.class, name = "espi:ReadingType"),
@JsonSubTypes.Type(value = TimeConfigurationDto.class, name = "espi:TimeConfiguration"),
@JsonSubTypes.Type(value = UsagePointDto.class, name = "espi:UsagePoint"),
@JsonSubTypes.Type(value = UsageSummaryDto.class, name = "espi:UsageSummary"),
// ESPI Customer resources (cust: namespace - top-level IdentifiedObject-based)
@JsonSubTypes.Type(value = CustomerDto.class, name = "cust:Customer"),
@JsonSubTypes.Type(value = CustomerAccountDto.class, name = "cust:CustomerAccount"),
@JsonSubTypes.Type(value = CustomerAgreementDto.class, name = "cust:CustomerAgreement"),
@JsonSubTypes.Type(value = EndDeviceDto.class, name = "cust:EndDevice"),
@JsonSubTypes.Type(value = MeterDto.class, name = "cust:Meter"),
@JsonSubTypes.Type(value = ProgramDateIdMappingsDto.class, name = "cust:ProgramDateIdMappings"),
// Note: TimeConfigurationDto supports BOTH espi: and cust: namespaces (same type in both schemas)
@JsonSubTypes.Type(value = TimeConfigurationDto.class, name = "cust:TimeConfiguration"),
@JsonSubTypes.Type(value = ServiceLocationDto.class, name = "cust:ServiceLocation"),
@JsonSubTypes.Type(value = StatementDto.class, name = "cust:Statement")
// TODO: Add when ServiceSupplierDto is implemented:
// @JsonSubTypes.Type(value = ServiceSupplierDto.class, name = "cust:ServiceSupplier")
})
@XmlAnyElement(lax = true)
@XmlElement(name = "content", namespace = "http://www.w3.org/2005/Atom")
Object content
Object content,

@XmlAttribute(name = "xmlns:espi")
String espiNamespace,

@XmlAttribute(name = "xmlns:cust")
String custNamespace
) {

/**
* Default constructor for JAXB.
* Compact constructor that auto-computes namespace attributes based on content type.
*/
public AtomEntryDto() {
this(null, null, null, null, null, null);
public AtomEntryDto {
// Auto-compute namespaces if not provided
if (content != null && espiNamespace == null && custNamespace == null) {
String packageName = content.getClass().getPackageName();
if (packageName.contains("dto.usage")) {
espiNamespace = "http://naesb.org/espi";
} else if (packageName.contains("dto.customer")) {
custNamespace = "http://naesb.org/espi/customer";
}
}
}

/**
* Constructor for basic entry data.
* Constructor for basic entry data without namespace (auto-computed).
*/
public AtomEntryDto(String id, String title, Object resource) {
public AtomEntryDto(String id, String title, OffsetDateTime published,
OffsetDateTime updated, List<LinkDto> links, Object content) {
this(id, title, published, updated, links, content, null, null);
}

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

this(id, title, now, now, null,
new AtomContentDto("application/xml", resource));
new AtomContentDto("application/xml", resource), null, null);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.xml.bind.annotation.*;
import jakarta.xml.bind.annotation.adapters.HexBinaryAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

/**
* TimeConfiguration DTO class for JAXB XML marshalling/unmarshalling.
*
* PROTOTYPE: Traditional approach using mutable class with JAXB.
* Alternative to Jackson XML record-based TimeConfigurationDtoJackson.
* TimeConfiguration DTO record for JAXB XML marshalling/unmarshalling.
*
* Represents time configuration parameters including timezone offset and
* daylight saving time rules for energy metering systems.
Expand All @@ -37,26 +36,48 @@
* @see <a href="https://www.naesb.org/ESPI_Standards.asp">NAESB ESPI 4.0</a>
*/
@XmlRootElement(name = "TimeConfiguration", namespace = "http://naesb.org/espi")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "TimeConfiguration", namespace = "http://naesb.org/espi", propOrder = {
"dstEndRule",
"dstOffset",
"dstStartRule",
"tzOffset"
})
public class TimeConfigurationDto {
public record TimeConfigurationDto(

@XmlTransient
@Schema(description = "Internal DTO identifier (not serialized to XML)")
Long id,

@XmlTransient
@Schema(description = "Resource identifier (mRID)", example = "550e8400-e29b-41d4-a716-446655440000")
String uuid,

@XmlElement(name = "dstEndRule", type = String.class)
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
@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 = "...")
byte[] dstEndRule,

@XmlElement(name = "dstOffset")
@Schema(description = "Daylight savings time offset from local standard time in seconds", example = "3600")
Long dstOffset,

@XmlElement(name = "dstStartRule", type = String.class)
@XmlJavaTypeAdapter(HexBinaryAdapter.class)
@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 = "...")
byte[] dstStartRule,

@XmlElement(name = "tzOffset")
@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")
Long tzOffset

private Long id;
private String uuid;
private byte[] dstEndRule;
private Long dstOffset;
private byte[] dstStartRule;
private Long tzOffset;
) {

/**
* Default constructor for JAXB.
*/
public TimeConfigurationDto() {
this(null, null, null, null, null, null);
}

/**
Expand All @@ -65,7 +86,7 @@ public TimeConfigurationDto() {
* @param tzOffset the timezone offset in seconds from UTC
*/
public TimeConfigurationDto(Long tzOffset) {
this.tzOffset = tzOffset;
this(null, null, null, null, null, tzOffset);
}

/**
Expand All @@ -75,104 +96,36 @@ public TimeConfigurationDto(Long tzOffset) {
* @param tzOffset the timezone offset in seconds from UTC
*/
public TimeConfigurationDto(String uuid, Long tzOffset) {
this.uuid = uuid;
this.tzOffset = tzOffset;
this(null, uuid, null, null, null, tzOffset);
}

/**
* Full constructor with all fields.
* Override dstEndRule getter to return cloned array for defensive copying.
*
* @param id internal DTO id
* @param uuid the resource identifier
* @param dstEndRule DST end rule bytes
* @param dstOffset DST offset in seconds
* @param dstStartRule DST start rule bytes
* @param tzOffset timezone offset in seconds from UTC
* @return cloned byte array or null
*/
public TimeConfigurationDto(Long id, String uuid, byte[] dstEndRule, Long dstOffset,
byte[] dstStartRule, Long tzOffset) {
this.id = id;
this.uuid = uuid;
this.dstEndRule = dstEndRule != null ? dstEndRule.clone() : null;
this.dstOffset = dstOffset;
this.dstStartRule = dstStartRule != null ? dstStartRule.clone() : null;
this.tzOffset = tzOffset;
}

// Getters with JAXB annotations

@XmlTransient
@Schema(description = "Internal DTO identifier (not serialized to XML)")
public Long getId() {
return id;
}

@XmlAttribute(name = "mRID")
@Schema(description = "Resource identifier (mRID)", example = "550e8400-e29b-41d4-a716-446655440000")
public String getUuid() {
return uuid;
}

@XmlElement(name = "dstEndRule", namespace = "http://naesb.org/espi")
@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 = "...")
public byte[] getDstEndRule() {
@Override
public byte[] dstEndRule() {
return dstEndRule != null ? dstEndRule.clone() : null;
}

@XmlElement(name = "dstOffset", namespace = "http://naesb.org/espi")
@Schema(description = "Daylight savings time offset from local standard time in seconds", example = "3600")
public Long getDstOffset() {
return dstOffset;
}

@XmlElement(name = "dstStartRule", namespace = "http://naesb.org/espi")
@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 = "...")
public byte[] getDstStartRule() {
/**
* Override dstStartRule getter to return cloned array for defensive copying.
*
* @return cloned byte array or null
*/
@Override
public byte[] dstStartRule() {
return dstStartRule != null ? dstStartRule.clone() : null;
}

@XmlElement(name = "tzOffset", namespace = "http://naesb.org/espi")
@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")
public Long getTzOffset() {
return tzOffset;
}

// Setters for JAXB unmarshalling

public void setId(Long id) {
this.id = id;
}

public void setUuid(String uuid) {
this.uuid = uuid;
}

public void setDstEndRule(byte[] dstEndRule) {
this.dstEndRule = dstEndRule != null ? dstEndRule.clone() : null;
}

public void setDstOffset(Long dstOffset) {
this.dstOffset = dstOffset;
}

public void setDstStartRule(byte[] dstStartRule) {
this.dstStartRule = dstStartRule != null ? dstStartRule.clone() : null;
}

public void setTzOffset(Long tzOffset) {
this.tzOffset = tzOffset;
}

// Utility methods
// TEMPORARY: @XmlTransient annotations prevent serialization with XmlAccessType.PROPERTY
// TODO: Remove when converting to record with XmlAccessType.FIELD (see issue #61)
// Utility methods (no @XmlTransient needed with FIELD access)

/**
* 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;
}
Expand All @@ -182,7 +135,6 @@ public Double getTzOffsetInHours() {
*
* @return DST offset in hours, or null if not set
*/
@XmlTransient
public Double getDstOffsetInHours() {
return dstOffset != null ? dstOffset / 3600.0 : null;
}
Expand All @@ -192,7 +144,6 @@ 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;
Expand All @@ -204,7 +155,6 @@ public Long getEffectiveOffset() {
*
* @return total offset in hours including DST
*/
@XmlTransient
public Double getEffectiveOffsetInHours() {
return getEffectiveOffset() / 3600.0;
}
Expand All @@ -214,7 +164,6 @@ 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;
Expand All @@ -225,7 +174,6 @@ public boolean hasDstRules() {
*
* @return true if DST offset is defined and non-zero, false otherwise
*/
@XmlTransient
public boolean isDstActive() {
return dstOffset != null && dstOffset != 0;
}
Expand Down
Loading
Loading