Repository navigation
fix(#247): export DC collection endpoints as Atom feeds instead of a bare List - #250
Merged
Merged
Conversation
…bare List
Every DC feed endpoint returned 500: the controllers passed a List<XxxDto> to
BaseExportService.exportDto, and JAXB cannot marshal a collection root
("ImmutableCollections$ListN ... not known to this context"). Even an empty
List.of() failed.
Feed assembly (openespi-common)
- BaseExportService.buildFeed/exportFeed: Atom feed with a UUID v5 id (from
the feed URL), title, published/updated, self link (espi-feed/<Resource>),
and one entry per resource: urn:uuid:<id>, title (description, else the
resource name), the entity's timestamps, self (espi-entry/) and up
(espi-feed/) links, and the mapped DTO as content. Stored links are used
only when they are absolute http(s) URLs; otherwise they are derived from
the feed URL, so they carry the real host and /DataCustodian context.
- newEntry(): usage exporters build UsageAtomEntryDto; the customer exporters
override it to build CustomerAtomEntryDto (the type in their JAXBContext).
- exportDto rejects a bare Collection with a message naming exportFeed.
- AtomFeedDto: published/updated get OffsetDateTimeAdapter (they were
written as empty elements); entries use @XmlElementRef so entries carry no
xsi:type="atom:UsageAtomEntry".
- LinkDto: comment pinning the rel, href, type field order, which JAXB uses
as the attribute order the CMD certification test requires.
- IdentifiedObject.ensureSelfLink/ensureUpLink passed (rel, href) to the
LinkType(href, rel) constructor, storing href="self" and href="up" on every
entity given an id.
Data Custodian
- All 13 list call sites in 10 controllers use exportFeed with the request
URL; ApplicationInformationService.export(List) takes the feed URL.
- AuthorizationController: both stubs (which returned null, i.e. 200 with an
empty body) are implemented: an Atom feed, and an Atom entry or 404.
- EspiXmlCaptureFilter (local/dev profiles): a 4xx/5xx response or an
exception writes <METHOD>_<path>.error.txt (status, request, exception
chain, body) beside the captured XML; the exception is still rethrown. A
relative capture dir resolves to the repository root.
Tests
- AtomFeedExportTest (real exporters): usage and customer feeds, empty feed,
link attribute order rel/href/type, collection rejection.
- FeedEndpointsIntegrationTest (DC, real exporters, H2 data): UsagePoint
feed with an entry, empty Authorization feed, Authorization 404.
- EspiXmlCaptureFilterTest: exception and 4xx capture, repo-root resolution.
Closes #247
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApplicationInformation and Authorization are readable with either DataCustodian_Admin_Access or ThirdParty_Admin_Access. Now that their feeds return data, the records a caller sees are scoped: - DataCustodian_Admin_Access: every record. - ThirdParty_Admin_Access: only records of its own application, matched on the token's client_id (Authorization via a new AuthorizationRepository.findAllByApplicationClientId query). - A ThirdParty admin token without client_id: nothing (fails closed). - A record of another application: 404, so its existence is not disclosed. The rule lives in one place (AdminCaller) for both controllers. AdminRowScopeTest covers feed and entry for each kind of caller, and fails if the check is bypassed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Third Party: UsagePointRESTRepositoryImpl built a JAXBContext from
AtomFeedDto alone. With AtomFeedDto.entries now an @XmlElementRef to the
abstract AtomEntryDto, JAXB needs a concrete entry type in the same context
("Invalid @XmlElementRef ... not known to this context"), so the TP
application context failed to start. Register UsageAtomEntryDto. That is
also what the TP needs to read usage feeds at all (#197: abstract entry
type).
- EspiXmlCaptureFilterTest.relativeDirResolvesToRepoRoot used AssertJ
Path.endsWith, which canonicalises the path and fails when the directory
does not exist, as on a fresh CI checkout; use endsWithRaw.
- EspiXmlCaptureFilter: explicit containment check before writing a capture
file (normalised path must sit directly in the run directory), plus a
traversal test. fileName() already stripped path separators; this makes
the guarantee explicit for code scanning.
Verified locally with no dev-xml-capture directory present: DC 196, TP 56
(2 skipped, pre-existing).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #247. Every Data Custodian collection ("feed") endpoint returned 500. The controllers handed JAXB a bare
List<XxxDto>, which has no root element (ImmutableCollections$ListN … not known to this context). Even an empty list failed. The endpoints now return ESPI Atom feeds.Feed assembly:
openespi-commonBaseExportService.buildFeed/exportFeed: one shared implementation, in common so E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 can reuse it.urn:uuidid (UUID v5 of the feed URL), title, published/updated,selflink (espi-feed/<Resource>).urn:uuid:<entity id>, title (description, else the resource name), the entity's timestamps,self(espi-entry/<Resource>) andup(espi-feed/<Resource>) links, and the DTO as content.http(s)URLs; otherwise they are derived from the request URL, so they include the host and/DataCustodian.newEntry(): usage exporters produceUsageAtomEntryDto;CustomerExportServiceandCustomerAccountExportServiceproduceCustomerAtomEntryDto, the type in their JAXB context.exportDtonow rejects a bareCollectionwith a message namingexportFeed, instead of an obscure JAXB error.AtomFeedDtobugs found by the new tests:published/updatedwere written as empty elements (noOffsetDateTimeAdapter);xsi:type="atom:UsageAtomEntry"(@XmlElementon the abstract type). It is now@XmlElementRef.LinkDto: a comment pins the field orderrel,href,type, because JAXB uses it as the attribute order, which the CMD certification test requires. A test guards it. I verified the test fails when the fields are reordered: Lombok's@AllArgsConstructorthen also swaps the values.IdentifiedObject.ensureSelfLink/ensureUpLinkpassed(rel, href)to theLinkType(href, rel)constructor. Every entity given an id storedhref="self"/href="up". Rows already saved in persistent dev databases keep those values; the feed code ignores non-absolute links, so feeds are correct either way.Data Custodian
exportFeedwith the request URL.ApplicationInformationService.export(List, …)now takes the feed URL.AuthorizationController: both methods were stubs returningnull(200 with an empty body). They now return an Atom feed, and an Atom entry or 404. Token values, authorization codes and state are@XmlTransientinAuthorizationDto, so they are never in the XML.EspiXmlCaptureFilter(local/dev profiles only): a 4xx/5xx response or an exception on/espi/1_1/resource/**writes<METHOD>_<path>.error.txt(status, request, exception chain, body) next to the captured XML indev-xml-capture/run-<timestamp>/; the exception is still rethrown. A relative capture directory now resolves to the repository root, so captures no longer land in the module folder when the DC is started fromopenespi-datacustodian/.Row-level scope for the admin resources
ApplicationInformation and Authorization accept
DataCustodian_Admin_AccessorThirdParty_Admin_Access. Now that their feeds return data, each caller is scoped by one rule (AdminCaller):DataCustodian_Admin_Accesssees every record;ThirdParty_Admin_Accesssees only its own application's records, matched on the token'sclient_id(newAuthorizationRepository.findAllByApplicationClientId);client_idsees nothing (fails closed);AdminRowScopeTestcovers the feed and the entry for each kind of caller. I verified it fails when the check is bypassed.Not changed here (follow-up)
Each resource is written directly under
<atom:entry>(<espi:UsagePoint>…) rather than inside<atom:content>, as in the REQ.21 sample feeds. That comes fromUsageAtomEntryDto/CustomerAtomEntryDtoand affects single entries as well as feeds. Changing it changes the wire format the Third Party parses (#197), so it should be its own change.Test plan
AtomFeedExportTest(common, real exporters, 5 tests): usage feed with two entries, empty feed, customer feed (cust namespace, noespi), link attribute orderrel,href,type, collection rejectionFeedEndpointsIntegrationTest(DC, real exporters, a UsagePoint saved in H2): feed with an entry and correct ids/links/type values; empty Authorization feed; Authorization 404EspiXmlCaptureFilterTest: +3 (exception chain captured and rethrown, 4xx captured, repo-root resolution)AdminRowScopeTest(8): row-level scope for ApplicationInformation and Authorization, feed and entrymvn verify: 195 pass. Common: 784 run; the only failures locally are 20 MySQL/PostgreSQL Testcontainers classes that need Docker (verified: allCould not find a valid Docker environment), plus one timing flake inCustomerAccountRepositoryTest(shouldUpdateTimestampsOnModification, two equal microsecond timestamps), which passed 3/3 on re-run. CI runs the Docker suites.localprofile,DataCustodian_Admin_Accesstoken): all 10 feed endpoints return 200 with a valid Atom feed (they were 500); an unknown Authorization id gives 404 and an.error.txtin the repo-root capture folder.🤖 Generated with Claude Code