Repository navigation
Description
Summary
First live run of the Data Custodian ESPI API (local profile, valid admin tokens from the AS): every collection ("feed") endpoint that has data returns 500. Authorization succeeds; the failure is in XML marshalling.
ERROR o.g.e.c.service.BaseExportService - Failed to marshal DTO: class java.util.ImmutableCollections$ListN
nor any of its super class is known to this context.
jakarta.xml.bind.JAXBException: class java.util.ImmutableCollections$ListN nor any of its super class is known to this context.
at ...common.service.BaseExportService.exportDto(BaseExportService.java:166)
at ...common.service.impl.ApplicationInformationServiceImpl.export(ApplicationInformationServiceImpl.java:124)
at ...datacustodian.web.api.ApplicationInformationRESTController.getAllApplicationInformation(...:82)
Root cause
The feed endpoints map entities to a List<XxxDto> (via stream().toList()) and pass the bare list to BaseExportService.exportDto(Object, OutputStream). JAXB can only marshal a root element, so a java.util.List always fails. An ESPI feed must instead be an Atom <feed> (AtomFeedDto) whose <entry> elements carry the resources in <content>, with self/up links per REQ.21.
Affected call sites (13, in 10 controllers under web/api/)
| Controller | Line(s) |
|---|---|
ApplicationInformationRESTController (via ApplicationInformationServiceImpl.export(List, …)) |
82 |
CustomerAccountRESTController |
89 |
CustomerRESTController |
89 |
ElectricPowerQualitySummaryRESTController |
95, 164 |
IntervalBlockRESTController |
89, 156 |
MeterReadingController |
94 |
ReadingTypeRESTController |
92 |
UsagePointController |
96, 176 |
UsageSummaryRESTController |
93, 159 |
Observed live:
GET /espi/1_1/resource/ApplicationInformation→ 500 with aDataCustodian_Admin_Accessor aThirdParty_Admin_Accesstoken;GET /espi/1_1/resource/UsagePoint→ 500 withDataCustodian_Admin_Access.
Single-entry (/{id}) endpoints marshal one DTO and are expected to work; they have not yet been verified live.
Related defect found in the same run
GET /espi/1_1/resource/Authorization returns 200 with an empty body and no Content-Type: AuthorizationController.getAllAuthorizations and getAuthorization are stubs that return null. An empty feed should be a valid Atom <feed> with no entries, and a missing entry should be a 404.
Why tests did not catch it
AbstractControllerMockTest mocks every *ExportService (@MockitoBean UsageExportService, ApplicationInformationExportService, …), so no controller test has ever marshalled a response. DevXmlSampleGenerator exports single entries only.
Proposed fix
- Feeds: wrap collections in an Atom feed (feed id/title/updated/self link plus one
<entry>per resource) instead of passing aListto JAXB. Do this once, in a shared helper, rather than in 13 places. E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 (E1 resource engine + Atom entry assembly) replaces these per-resource controllers. Either land E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 for the affected resources, or add an interimexportFeed(List<?>, …)inBaseExportServicethat the current controllers call until E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 retires them. - Authorization: implement the two stub methods, or return an empty feed and a 404 until they are implemented.
- Tests: at least one MockMvc test per feed endpoint that uses the real export services with sample data, and validates the body against
schema/ESPI_4.0/espi.xsdandatom.xsd(now packaged in OpenESPI-Common, feat(ui): shared Green Button Alliance theme and Third Party layout (prototype) #244). Also one test for an empty feed. - Diagnostics:
EspiXmlCaptureFilter(local/dev profiles,dev-xml-capture/run-*/) captures only non-empty XML bodies. A marshalling failure throws before anything is written, so failures leave no trace on disk. Extend it so that a failed/espi/1_1/resource/**request writesGET_<path>.error.txt, containing the status, the exception chain and the DTO type that failed, next to the captured XML. In dev profiles only, also return an RFC 9457application/problem+jsonbody naming the failing type instead of a bare 500.
Reproduce
TOKEN=$(curl -s -u data_custodian_admin:dc-secret -d grant_type=client_credentials \
-d scope=DataCustodian_Admin_Access http://localhost:9999/oauth2/token | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8081/DataCustodian/espi/1_1/resource/ApplicationInformation # 500
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8081/DataCustodian/espi/1_1/resource/Authorization # 200, empty body(AS, DC and TP on the local profile, with the DC local client secret dc-secret from #240.)
References
#187 (E1 resource engine), #119 (canonical build plan, EspiXmlCaptureFilter), #141 (TP Authorization stubs), #197 (TP cannot unmarshal Atom feeds: the receiving side of the same format)
🤖 Generated with Claude Code
Activity
fix(#247): row-level scope for the ESPI admin resources
fix(#247): fix the Third Party JAXB context and CI-only test failures