Skip to content

Commit 7a25404

Browse files
dfcoffinclaude
andauthored
feat: ESPI 4.0 Schema Compliance - Phase 6: MeterReading (#72)
Phase 6 of ESPI 4.0 schema compliance focuses on MeterReading, which per the specification has NO child elements - only Atom link relationships. Changes: - MeterReadingDto: Already compliant, updated documentation - Has NO child elements per espi.xsd specification - Relationships to ReadingType and IntervalBlock via Atom links only - Removed unused imports (OffsetDateTime, List, LinkDto) - Added ESPI 4.0 specification documentation - MeterReadingMapper: Simplified for empty DTO - Removed BaseMapperUtils dependency (not needed) - Removed IntervalBlockMapper and ReadingTypeMapper dependencies - Removed updateEntity method (read-only operations only) - Added toEntity mappings for all audit/relationship fields - MeterReadingRepository: Optimized for indexed queries only - Kept: findAllIds, findAllIdsByUsagePointId (indexed on usage_point_id) - Removed: deleteById override, findByRelatedHref, findAllRelated, findAllIdsByXpath2, findIdByXpath (non-indexed complex queries) - Removed @Modifying, @transactional imports - MeterReadingRepositoryTest: Removed obsolete tests - Removed tests for non-indexed queries (relatedHref, xpath) - Updated empty results test - All 23 tests passing Related Issues: - Part of Issue #28 (ESPI 4.0 Schema Compliance) All 541 openespi-common tests passing. Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent a82d10d commit 7a25404

File tree

4 files changed

+27
-168
lines changed

4 files changed

+27
-168
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@
1919

2020
package org.greenbuttonalliance.espi.common.dto.usage;
2121

22-
import org.greenbuttonalliance.espi.common.dto.atom.LinkDto;
23-
2422
import jakarta.xml.bind.annotation.*;
25-
import java.time.OffsetDateTime;
26-
import java.util.List;
2723

2824
/**
2925
* MeterReading DTO record for JAXB XML marshalling/unmarshalling.
30-
*
31-
* Represents a meter reading containing interval blocks and reading types.
32-
* Supports Atom protocol XML wrapping.
26+
*
27+
* Represents a meter reading - a set of values obtained from the meter.
28+
* Per ESPI 4.0 specification, MeterReading extends IdentifiedObject but
29+
* contains NO child elements. Relationships to ReadingType and IntervalBlock
30+
* are expressed via Atom links, not embedded XML elements.
31+
*
32+
* Complies with espi.xsd MeterReading complexType definition.
33+
*
34+
* @see <a href="https://www.naesb.org/ESPI_Standards.asp">NAESB ESPI 4.0</a>
3335
*/
3436
@XmlRootElement(name = "MeterReading", namespace = "http://naesb.org/espi")
3537
@XmlAccessorType(XmlAccessType.FIELD)

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

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,20 @@
2222
import org.greenbuttonalliance.espi.common.domain.usage.MeterReadingEntity;
2323
import org.greenbuttonalliance.espi.common.dto.usage.MeterReadingDto;
2424
import org.greenbuttonalliance.espi.common.mapper.BaseIdentifiedObjectMapper;
25-
import org.greenbuttonalliance.espi.common.mapper.BaseMapperUtils;
2625
import org.greenbuttonalliance.espi.common.mapper.DateTimeMapper;
2726
import org.mapstruct.Mapper;
2827
import org.mapstruct.Mapping;
29-
import org.mapstruct.MappingTarget;
3028

3129
/**
3230
* MapStruct mapper for converting between MeterReadingEntity and MeterReadingDto.
33-
*
34-
* Handles the conversion between the JPA entity used for persistence and the DTO
31+
*
32+
* Per ESPI 4.0 specification, MeterReading has NO child elements - only relationships
33+
* expressed via Atom links. The DTO contains NO fields beyond id/uuid.
34+
*
35+
* Handles the conversion between the JPA entity used for persistence and the DTO
3536
* used for JAXB XML marshalling in the Green Button API.
3637
*/
37-
@Mapper(componentModel = "spring", uses = {
38-
DateTimeMapper.class,
39-
BaseMapperUtils.class,
40-
IntervalBlockMapper.class,
41-
ReadingTypeMapper.class
42-
})
38+
@Mapper(componentModel = "spring", uses = {DateTimeMapper.class})
4339
public interface MeterReadingMapper {
4440

4541
/**
@@ -55,33 +51,21 @@ public interface MeterReadingMapper {
5551

5652
/**
5753
* Converts a MeterReadingDto to a MeterReadingEntity.
58-
* Maps all related DTOs to their corresponding entities.
59-
*
54+
* Since MeterReading has no child elements, only audit and relationship fields are managed.
55+
*
6056
* @param dto the meter reading DTO
6157
* @return the meter reading entity
6258
*/
6359
@Mapping(target = "id", source = "uuid", qualifiedByName = "stringToUuid")
64-
@Mapping(target = "usagePoint", ignore = true)
65-
@Mapping(target = "readingType", ignore = true)
66-
@Mapping(target = "intervalBlocks", ignore = true)
67-
@Mapping(target = "relatedLinks", ignore = true)
68-
@Mapping(target = "selfLink", ignore = true)
69-
@Mapping(target = "upLink", ignore = true)
60+
@Mapping(target = "created", ignore = true) // Audit field managed by persistence
61+
@Mapping(target = "updated", ignore = true) // Audit field managed by persistence
62+
@Mapping(target = "published", ignore = true) // Audit field managed by persistence
63+
@Mapping(target = "description", ignore = true) // Managed separately
64+
@Mapping(target = "selfLink", ignore = true) // Link managed separately
65+
@Mapping(target = "upLink", ignore = true) // Link managed separately
66+
@Mapping(target = "relatedLinks", ignore = true) // Links managed separately
67+
@Mapping(target = "usagePoint", ignore = true) // Relationship managed separately
68+
@Mapping(target = "readingType", ignore = true) // Relationship managed separately
69+
@Mapping(target = "intervalBlocks", ignore = true) // Relationship managed separately
7070
MeterReadingEntity toEntity(MeterReadingDto dto);
71-
72-
/**
73-
* Updates an existing MeterReadingEntity with data from a MeterReadingDto.
74-
* Useful for merge operations where the entity ID should be preserved.
75-
*
76-
* @param dto the source DTO
77-
* @param entity the target entity to update
78-
*/
79-
@Mapping(target = "id", ignore = true)
80-
@Mapping(target = "usagePoint", ignore = true)
81-
@Mapping(target = "readingType", ignore = true)
82-
@Mapping(target = "intervalBlocks", ignore = true)
83-
@Mapping(target = "relatedLinks", ignore = true)
84-
@Mapping(target = "selfLink", ignore = true)
85-
@Mapping(target = "upLink", ignore = true)
86-
void updateEntity(MeterReadingDto dto, @MappingTarget MeterReadingEntity entity);
8771
}

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/repositories/usage/MeterReadingRepository.java

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,50 +21,20 @@
2121

2222
import org.greenbuttonalliance.espi.common.domain.usage.MeterReadingEntity;
2323
import org.springframework.data.jpa.repository.JpaRepository;
24-
import org.springframework.data.jpa.repository.Modifying;
2524
import org.springframework.data.jpa.repository.Query;
2625
import org.springframework.data.repository.query.Param;
2726
import org.springframework.stereotype.Repository;
28-
import org.springframework.transaction.annotation.Transactional;
2927

3028
import java.util.List;
31-
import java.util.Optional;
3229
import java.util.UUID;
3330

3431
@Repository
3532
public interface MeterReadingRepository extends JpaRepository<MeterReadingEntity, UUID> {
3633

37-
// JpaRepository provides: save(), findById(), findAll(), deleteById(), etc.
38-
39-
@Modifying
40-
@Transactional
41-
@Query("DELETE FROM MeterReadingEntity m WHERE m.id = :id")
42-
void deleteById(@Param("id") UUID id);
43-
4434
@Query("SELECT m.id FROM MeterReadingEntity m")
4535
List<UUID> findAllIds();
4636

47-
// findById is already provided by JpaRepository<MeterReadingEntity, UUID>
48-
// Optional<MeterReadingEntity> findById(UUID id) is inherited
49-
50-
// deleteById is already provided by JpaRepository<MeterReadingEntity, UUID>
51-
// void deleteById(UUID id) is inherited
52-
53-
// Custom method for createOrReplaceByUUID - should be implemented in service layer
54-
55-
@Query("SELECT m FROM MeterReadingEntity m join m.relatedLinks link WHERE link.href = :href")
56-
List<MeterReadingEntity> findByRelatedHref(@Param("href") String href);
57-
58-
@Query("SELECT readingType FROM ReadingTypeEntity readingType WHERE readingType.selfLink.href in (:relatedLinkHrefs)")
59-
List<Object> findAllRelated(@Param("relatedLinkHrefs") List<String> relatedLinkHrefs);
60-
6137
@Query("SELECT m.id FROM MeterReadingEntity m WHERE m.usagePoint.id = :usagePointId")
6238
List<UUID> findAllIdsByUsagePointId(@Param("usagePointId") UUID usagePointId);
6339

64-
@Query("SELECT DISTINCT m.id FROM UsagePointEntity u, MeterReadingEntity m WHERE u.retailCustomer.id = :o1Id AND m.usagePoint.id = :o2Id")
65-
List<UUID> findAllIdsByXpath2(@Param("o1Id") UUID o1Id, @Param("o2Id") UUID o2Id);
66-
67-
@Query("SELECT DISTINCT m.id FROM UsagePointEntity u, MeterReadingEntity m WHERE u.retailCustomer.id = :o1Id AND m.usagePoint.id = :o2Id AND m.id = :o3Id")
68-
Optional<UUID> findIdByXpath(@Param("o1Id") UUID o1Id, @Param("o2Id") UUID o2Id, @Param("o3Id") UUID o3Id);
69-
7040
}

openespi-common/src/test/java/org/greenbuttonalliance/espi/common/repositories/usage/MeterReadingRepositoryTest.java

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -267,41 +267,6 @@ void shouldFindAllMeterReadingIds() {
267267
);
268268
}
269269

270-
@Test
271-
@DisplayName("Should find meter readings by related href")
272-
void shouldFindMeterReadingsByRelatedHref() {
273-
// Arrange - Create a simple meter reading without related links for now
274-
MeterReadingEntity meterReading = TestDataBuilders.createValidMeterReading();
275-
meterReading.setDescription("Meter Reading for Related Href Test");
276-
meterReadingRepository.save(meterReading);
277-
flushAndClear();
278-
279-
// Act - Test the query method with a non-existent href (should return empty)
280-
List<MeterReadingEntity> results = meterReadingRepository.findByRelatedHref("/espi/1_1/resource/IntervalBlock/123");
281-
282-
// Assert - Should return empty list since no meter reading has this related href
283-
assertThat(results).isEmpty();
284-
}
285-
286-
@Test
287-
@DisplayName("Should find all related entities")
288-
void shouldFindAllRelatedEntities() {
289-
// Arrange
290-
List<String> relatedLinkHrefs = List.of(
291-
"/espi/1_1/resource/ReadingType/1",
292-
"/espi/1_1/resource/ReadingType/2",
293-
"/espi/1_1/resource/ReadingType/3"
294-
);
295-
296-
// Act
297-
List<Object> results = meterReadingRepository.findAllRelated(relatedLinkHrefs);
298-
299-
// Assert
300-
assertThat(results).isNotNull();
301-
// Note: This query looks for ReadingType entities with matching self links
302-
// The actual results depend on existing data in the test database
303-
}
304-
305270
@Test
306271
@DisplayName("Should find all meter reading IDs by usage point ID")
307272
void shouldFindAllMeterReadingIdsByUsagePointId() {
@@ -326,73 +291,11 @@ void shouldFindAllMeterReadingIdsByUsagePointId() {
326291
assertThat(meterReadingIds).contains(savedMeterReadings.get(0).getId(), savedMeterReadings.get(1).getId());
327292
}
328293

329-
@Test
330-
@DisplayName("Should find all meter reading IDs by xpath2")
331-
void shouldFindAllMeterReadingIdsByXpath2() {
332-
// Arrange
333-
RetailCustomerEntity retailCustomer = TestDataBuilders.createValidRetailCustomer();
334-
retailCustomer.setUsername("[email protected]");
335-
RetailCustomerEntity savedCustomer = retailCustomerRepository.save(retailCustomer);
336-
337-
UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint();
338-
usagePoint.setDescription("Usage Point for Xpath2");
339-
usagePoint.setRetailCustomer(savedCustomer);
340-
UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint);
341-
342-
MeterReadingEntity meterReading1 = TestDataBuilders.createValidMeterReadingWithUsagePoint(savedUsagePoint);
343-
meterReading1.setDescription("Meter Reading 1 for Xpath2");
344-
MeterReadingEntity meterReading2 = TestDataBuilders.createValidMeterReadingWithUsagePoint(savedUsagePoint);
345-
meterReading2.setDescription("Meter Reading 2 for Xpath2");
346-
347-
List<MeterReadingEntity> savedMeterReadings = meterReadingRepository.saveAll(List.of(meterReading1, meterReading2));
348-
flushAndClear();
349-
350-
// Act
351-
List<UUID> meterReadingIds = meterReadingRepository.findAllIdsByXpath2(savedCustomer.getId(), savedUsagePoint.getId());
352-
353-
// Assert
354-
assertThat(meterReadingIds).hasSize(2);
355-
assertThat(meterReadingIds).contains(savedMeterReadings.get(0).getId(), savedMeterReadings.get(1).getId());
356-
}
357-
358-
@Test
359-
@DisplayName("Should find meter reading ID by xpath")
360-
void shouldFindMeterReadingIdByXpath() {
361-
// Arrange
362-
RetailCustomerEntity retailCustomer = TestDataBuilders.createValidRetailCustomer();
363-
retailCustomer.setUsername("[email protected]");
364-
RetailCustomerEntity savedCustomer = retailCustomerRepository.save(retailCustomer);
365-
366-
UsagePointEntity usagePoint = TestDataBuilders.createValidUsagePoint();
367-
usagePoint.setDescription("Usage Point for Xpath");
368-
usagePoint.setRetailCustomer(savedCustomer);
369-
UsagePointEntity savedUsagePoint = usagePointRepository.save(usagePoint);
370-
371-
MeterReadingEntity meterReading = TestDataBuilders.createValidMeterReadingWithUsagePoint(savedUsagePoint);
372-
meterReading.setDescription("Meter Reading for Xpath");
373-
MeterReadingEntity savedMeterReading = meterReadingRepository.save(meterReading);
374-
flushAndClear();
375-
376-
// Act
377-
Optional<UUID> result = meterReadingRepository.findIdByXpath(
378-
savedCustomer.getId(),
379-
savedUsagePoint.getId(),
380-
savedMeterReading.getId()
381-
);
382-
383-
// Assert
384-
assertThat(result).isPresent();
385-
assertThat(result.get()).isEqualTo(savedMeterReading.getId());
386-
}
387-
388294
@Test
389295
@DisplayName("Should handle empty results gracefully")
390296
void shouldHandleEmptyResultsGracefully() {
391297
// Act & Assert
392-
assertThat(meterReadingRepository.findByRelatedHref("nonexistent-href")).isEmpty();
393298
assertThat(meterReadingRepository.findAllIdsByUsagePointId(UUID.randomUUID())).isEmpty();
394-
assertThat(meterReadingRepository.findAllIdsByXpath2(UUID.randomUUID(), UUID.randomUUID())).isEmpty();
395-
assertThat(meterReadingRepository.findIdByXpath(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())).isEmpty();
396299
}
397300
}
398301

0 commit comments

Comments
 (0)