Skip to content

Commit cd53b6d

Browse files
dfcoffinclaude
andauthored
refactor: Phase 9 - AggregatedNodeRef ESPI 4.0 schema compliance (#75)
Implements ESPI 4.0 schema compliance for AggregatedNodeRef (espi.xsd:1570-1601): Key Changes: - Changed ID from UUID to Long (extends Object, not IdentifiedObject) - Fixed PnodeRef relationship: @manytoone@manytomany with join table (Per XSD line 1597: pnodeRef has minOccurs="0" maxOccurs="unbounded") - Moved table creation to vendor-specific V2 migrations (auto-increment) - Removed 13 non-indexed repository queries, kept only 2 indexed queries - Removed updateEntity mapper method (read-only operations) Entity Changes: - AggregatedNodeRefEntity: UUID id → Long id, PnodeRefEntity → List<PnodeRefEntity> - Added @manytomany join table: aggregated_node_ref_pnode_refs DTO Changes: - AggregatedNodeRefDto: PnodeRefDto → List<PnodeRefDto> - Updated all constructors and factory methods Database Changes: - Removed aggregated_node_refs from V3 (vendor-neutral) - Added to V2 MySQL/PostgreSQL/H2 with proper BIGINT AUTO_INCREMENT/BIGSERIAL - Added aggregated_node_ref_pnode_refs join table with composite primary key Tests: - Updated AggregatedNodeRefRepositoryTest with comprehensive test coverage - All 537 tests pass Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent 041a213 commit cd53b6d

File tree

10 files changed

+436
-221
lines changed

10 files changed

+436
-221
lines changed

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/AggregatedNodeRefEntity.java

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@
2323
import lombok.Getter;
2424
import lombok.NoArgsConstructor;
2525
import lombok.Setter;
26-
import org.hibernate.annotations.JdbcTypeCode;
2726
import org.hibernate.proxy.HibernateProxy;
28-
import org.hibernate.type.SqlTypes;
2927

28+
import java.util.ArrayList;
29+
import java.util.List;
3030
import java.util.Objects;
31-
import java.util.UUID;
3231

3332
/**
3433
* JPA entity for AggregatedNodeRef (Aggregated Node Reference).
@@ -48,12 +47,12 @@ public class AggregatedNodeRefEntity {
4847

4948
/**
5049
* Primary key identifier.
50+
* AggregatedNodeRef extends Object (not IdentifiedObject), so uses Long ID.
5151
*/
5252
@Id
53-
@GeneratedValue(strategy = GenerationType.UUID)
54-
@JdbcTypeCode(SqlTypes.CHAR)
55-
@Column(length = 36, columnDefinition = "char(36)", updatable = false, nullable = false)
56-
private UUID id;
53+
@GeneratedValue(strategy = GenerationType.IDENTITY)
54+
@Column(updatable = false, nullable = false)
55+
private Long id;
5756

5857
/**
5958
* Type of the aggregated node.
@@ -84,12 +83,16 @@ public class AggregatedNodeRefEntity {
8483
private Long endEffectiveDate;
8584

8685
/**
87-
* Associated pricing node reference for this aggregated node.
88-
* Each aggregated node references an underlying pricing node.
86+
* Associated pricing node references for this aggregated node.
87+
* Per ESPI 4.0 XSD (espi.xsd:1597), each aggregated node can reference 0 to many pricing nodes.
8988
*/
90-
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
91-
@JoinColumn(name = "pnode_ref_id")
92-
private PnodeRefEntity pnodeRef;
89+
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
90+
@JoinTable(
91+
name = "aggregated_node_ref_pnode_refs",
92+
joinColumns = @JoinColumn(name = "aggregated_node_ref_id"),
93+
inverseJoinColumns = @JoinColumn(name = "pnode_ref_id")
94+
)
95+
private List<PnodeRefEntity> pnodeRefs = new ArrayList<>();
9396

9497
/**
9598
* Usage point that owns this aggregated node reference.
@@ -102,21 +105,21 @@ public class AggregatedNodeRefEntity {
102105
/**
103106
* Constructor with all fields.
104107
*/
105-
public AggregatedNodeRefEntity(String anodeType, String ref, Long startEffectiveDate, Long endEffectiveDate,
106-
PnodeRefEntity pnodeRef, UsagePointEntity usagePoint) {
108+
public AggregatedNodeRefEntity(String anodeType, String ref, Long startEffectiveDate, Long endEffectiveDate,
109+
List<PnodeRefEntity> pnodeRefs, UsagePointEntity usagePoint) {
107110
this.anodeType = anodeType;
108111
this.ref = ref;
109112
this.startEffectiveDate = startEffectiveDate;
110113
this.endEffectiveDate = endEffectiveDate;
111-
this.pnodeRef = pnodeRef;
114+
this.pnodeRefs = pnodeRefs != null ? pnodeRefs : new ArrayList<>();
112115
this.usagePoint = usagePoint;
113116
}
114117

115118
/**
116119
* Constructor with basic fields.
117120
*/
118-
public AggregatedNodeRefEntity(String anodeType, String ref, PnodeRefEntity pnodeRef, UsagePointEntity usagePoint) {
119-
this(anodeType, ref, null, null, pnodeRef, usagePoint);
121+
public AggregatedNodeRefEntity(String anodeType, String ref, List<PnodeRefEntity> pnodeRefs, UsagePointEntity usagePoint) {
122+
this(anodeType, ref, null, null, pnodeRefs, usagePoint);
120123
}
121124

122125
/**
@@ -143,14 +146,18 @@ public String getDisplayName() {
143146
}
144147

145148
/**
146-
* Gets display name including the associated pricing node.
147-
*
148-
* @return formatted display name with pricing node
149+
* Gets display name including the associated pricing nodes.
150+
*
151+
* @return formatted display name with pricing nodes
149152
*/
150153
public String getFullDisplayName() {
151154
String aggregatedDisplay = getDisplayName();
152-
if (pnodeRef != null) {
153-
return aggregatedDisplay + " -> " + pnodeRef.getDisplayName();
155+
if (pnodeRefs != null && !pnodeRefs.isEmpty()) {
156+
String pnodeNames = pnodeRefs.stream()
157+
.map(PnodeRefEntity::getDisplayName)
158+
.reduce((a, b) -> a + ", " + b)
159+
.orElse("");
160+
return aggregatedDisplay + " -> [" + pnodeNames + "]";
154161
}
155162
return aggregatedDisplay;
156163
}

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

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,30 @@
2121

2222
import jakarta.xml.bind.annotation.*;
2323

24+
import java.util.ArrayList;
25+
import java.util.List;
26+
2427
/**
2528
* AggregatedNodeRef DTO record for JAXB XML marshalling/unmarshalling.
26-
*
29+
*
2730
* Represents a reference to an aggregated node in the electrical grid.
2831
* Used within UsagePoint to specify aggregated pricing/load zones.
29-
*
32+
*
33+
* Per ESPI 4.0 XSD (espi.xsd:1597), pnodeRef has minOccurs="0" maxOccurs="unbounded"
34+
*
3035
* Part of the NAESB ESPI UsagePoint structure for aggregated node references.
3136
*/
3237
@XmlAccessorType(XmlAccessType.PROPERTY)
3338
@XmlType(name = "AggregatedNodeRef", namespace = "http://naesb.org/espi", propOrder = {
3439
"anodeType", "ref", "startEffectiveDate", "endEffectiveDate", "pnodeRef"
3540
})
3641
public record AggregatedNodeRefDto(
37-
42+
3843
String anodeType,
3944
String ref,
4045
Long startEffectiveDate,
4146
Long endEffectiveDate,
42-
PnodeRefDto pnodeRef
47+
List<PnodeRefDto> pnodeRef
4348
) {
4449

4550
/**
@@ -78,32 +83,33 @@ public Long getEndEffectiveDate() {
7883
}
7984

8085
/**
81-
* Pricing node reference associated with this aggregated node.
82-
* Contains the underlying pricing node that contributes to the aggregated node.
86+
* Pricing node references associated with this aggregated node.
87+
* Contains the underlying pricing nodes that contribute to the aggregated node.
88+
* Per ESPI 4.0 XSD (espi.xsd:1597), supports 0 to many pricing node references.
8389
*/
8490
@XmlElement(name = "pnodeRef")
85-
public PnodeRefDto getPnodeRef() {
86-
return pnodeRef;
91+
public List<PnodeRefDto> getPnodeRef() {
92+
return pnodeRef != null ? pnodeRef : new ArrayList<>();
8793
}
88-
94+
8995
/**
9096
* Default constructor for JAXB.
9197
*/
9298
public AggregatedNodeRefDto() {
93-
this(null, null, null, null, null);
99+
this(null, null, null, null, new ArrayList<>());
94100
}
95-
101+
96102
/**
97103
* Constructor with aggregated node reference and type.
98104
*/
99105
public AggregatedNodeRefDto(String anodeType, String ref) {
100-
this(anodeType, ref, null, null, null);
106+
this(anodeType, ref, null, null, new ArrayList<>());
101107
}
102-
108+
103109
/**
104-
* Constructor with aggregated node reference, type, and pricing node.
110+
* Constructor with aggregated node reference, type, and pricing nodes.
105111
*/
106-
public AggregatedNodeRefDto(String anodeType, String ref, PnodeRefDto pnodeRef) {
112+
public AggregatedNodeRefDto(String anodeType, String ref, List<PnodeRefDto> pnodeRef) {
107113
this(anodeType, ref, null, null, pnodeRef);
108114
}
109115

@@ -120,40 +126,40 @@ public boolean isValid() {
120126

121127
/**
122128
* Creates an AggregatedNodeRef with current validity period.
123-
*
129+
*
124130
* @param anodeType the type of aggregated node
125131
* @param ref aggregated node reference
126132
* @return AggregatedNodeRef valid from now
127133
*/
128134
public static AggregatedNodeRefDto createCurrent(String anodeType, String ref) {
129135
long currentTime = System.currentTimeMillis() / 1000;
130-
return new AggregatedNodeRefDto(anodeType, ref, currentTime, null, null);
136+
return new AggregatedNodeRefDto(anodeType, ref, currentTime, null, new ArrayList<>());
131137
}
132-
138+
133139
/**
134-
* Creates an AggregatedNodeRef with current validity period and pricing node reference.
135-
*
140+
* Creates an AggregatedNodeRef with current validity period and pricing node references.
141+
*
136142
* @param anodeType the type of aggregated node
137143
* @param ref aggregated node reference
138-
* @param pnodeRef associated pricing node reference
144+
* @param pnodeRef associated pricing node references
139145
* @return AggregatedNodeRef valid from now
140146
*/
141-
public static AggregatedNodeRefDto createCurrent(String anodeType, String ref, PnodeRefDto pnodeRef) {
147+
public static AggregatedNodeRefDto createCurrent(String anodeType, String ref, List<PnodeRefDto> pnodeRef) {
142148
long currentTime = System.currentTimeMillis() / 1000;
143149
return new AggregatedNodeRefDto(anodeType, ref, currentTime, null, pnodeRef);
144150
}
145-
151+
146152
/**
147153
* Creates an AggregatedNodeRef with specified validity period.
148-
*
154+
*
149155
* @param anodeType the type of aggregated node
150156
* @param ref aggregated node reference
151157
* @param startEffectiveDate start of validity period (epoch seconds)
152158
* @param endEffectiveDate end of validity period (epoch seconds, null for indefinite)
153-
* @param pnodeRef associated pricing node reference
159+
* @param pnodeRef associated pricing node references
154160
* @return AggregatedNodeRef with specified validity
155161
*/
156-
public static AggregatedNodeRefDto create(String anodeType, String ref, Long startEffectiveDate, Long endEffectiveDate, PnodeRefDto pnodeRef) {
162+
public static AggregatedNodeRefDto create(String anodeType, String ref, Long startEffectiveDate, Long endEffectiveDate, List<PnodeRefDto> pnodeRef) {
157163
return new AggregatedNodeRefDto(anodeType, ref, startEffectiveDate, endEffectiveDate, pnodeRef);
158164
}
159165
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ public static AggregatedNodeRefsDto createLoadZoneRefs() {
147147
// Create pricing node references for the load zones
148148
PnodeRefDto northPnode = PnodeRefDto.createCurrent("HUB", "NORTH_HUB_PNODE");
149149
PnodeRefDto southPnode = PnodeRefDto.createCurrent("HUB", "SOUTH_HUB_PNODE");
150-
150+
151151
return new AggregatedNodeRefsDto(
152-
AggregatedNodeRefDto.createCurrent("LOAD_ZONE", "LOAD_ZONE_NORTH", northPnode),
153-
AggregatedNodeRefDto.createCurrent("LOAD_ZONE", "LOAD_ZONE_SOUTH", southPnode)
152+
AggregatedNodeRefDto.createCurrent("LOAD_ZONE", "LOAD_ZONE_NORTH", List.of(northPnode)),
153+
AggregatedNodeRefDto.createCurrent("LOAD_ZONE", "LOAD_ZONE_SOUTH", List.of(southPnode))
154154
);
155155
}
156156
}

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/mapper/usage/AggregatedNodeRefMapper.java

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import org.greenbuttonalliance.espi.common.dto.usage.AggregatedNodeRefDto;
2424
import org.mapstruct.Mapper;
2525
import org.mapstruct.Mapping;
26-
import org.mapstruct.MappingTarget;
2726

2827
/**
2928
* MapStruct mapper for converting between AggregatedNodeRefEntity and AggregatedNodeRefDto.
@@ -41,7 +40,7 @@ public interface AggregatedNodeRefMapper {
4140
@Mapping(target = "ref", source = "ref")
4241
@Mapping(target = "startEffectiveDate", source = "startEffectiveDate")
4342
@Mapping(target = "endEffectiveDate", source = "endEffectiveDate")
44-
@Mapping(target = "pnodeRef", source = "pnodeRef")
43+
@Mapping(target = "pnodeRef", source = "pnodeRefs")
4544
AggregatedNodeRefDto toDto(AggregatedNodeRefEntity entity);
4645

4746
/**
@@ -56,16 +55,7 @@ public interface AggregatedNodeRefMapper {
5655
@Mapping(target = "ref", source = "ref")
5756
@Mapping(target = "startEffectiveDate", source = "startEffectiveDate")
5857
@Mapping(target = "endEffectiveDate", source = "endEffectiveDate")
59-
@Mapping(target = "pnodeRef", source = "pnodeRef")
58+
@Mapping(target = "pnodeRefs", source = "pnodeRef")
6059
AggregatedNodeRefEntity toEntity(AggregatedNodeRefDto dto);
6160

62-
/**
63-
* Updates an existing AggregatedNodeRefEntity with data from an AggregatedNodeRefDto.
64-
*
65-
* @param dto the source DTO
66-
* @param entity the target entity to update
67-
*/
68-
@Mapping(target = "id", ignore = true)
69-
@Mapping(target = "usagePoint", ignore = true)
70-
void updateEntity(AggregatedNodeRefDto dto, @MappingTarget AggregatedNodeRefEntity entity);
7161
}

0 commit comments

Comments
 (0)