Skip to content

Get all versions of a record#9227

Draft
fxprunayre wants to merge 2 commits intomainfrom
44-record-versions-list
Draft

Get all versions of a record#9227
fxprunayre wants to merge 2 commits intomainfrom
44-record-versions-list

Conversation

@fxprunayre
Copy link
Copy Markdown
Member

@fxprunayre fxprunayre commented Apr 3, 2026

Quite often datasets are updated on a regular basis and new versions are released and past ones are flagged as superseded/deprecated/archived. Some users have a need of being able to get an overview of all versions of a datasets.

So far, one way to get that overview was to group all versions in a collection but that adds extra work to encode and organize the collection properly eg. https://sdi.eea.europa.eu/catalogue/srv/eng/catalog.search#/metadata/3ccf2fe3-40e3-4968-a54c-5a59510fa5a3

The catalogue is also able to navigate to the next revision of record when a revisionOf association is created between 2 records. eg. https://sdi.eea.europa.eu/catalogue/srv/eng/catalog.search#/metadata/607a6413-b3bf-456f-9ed2-6537006c4771

The main goal is to be able to list for a dataset all versions and more easily navigate to next/previous versions using the API. first/next/previous/last are also concept used in DCAT3 so this can help on the mapping.

AI/LLM tools were used in this contribution.

API for navigating in versions

The API provide an endpoint for retrieving verions:

  • GET /records/{metadataUuid}/associated?type=versions returns all past and more recent versions
  • GET /records/{metadataUuid}/associated?type=next
  • GET /records/{metadataUuid}/associated?type=previous

Those associations can also be retrieved when searching using relatedType parameter.

image

Encoding of revision

In ISO19115-3, associated resource can link to revision using the association type revisionOf link. Encoding is made using:

<mri:associatedResource>
  <mri:MD_AssociatedResource>
    <mri:associationType>
      <mri:DS_AssociationTypeCode codeListValue="revisionOf"/>
    </mri:associationType>
    <mri:metadataReference uuidref="65d546c6-3577-4938-8322-5d95188779a1"/>
  </mri:MD_AssociatedResource>
</mri:associatedResource>

In the index, this information is stored in agg_associated_revisionOf.

So versions are only supported in ISO19115-3.

Versions navigation

The record view allows to navigate from version to version (as before)

image

and also display the full list in the lineage section:

image

Test data

See related-versions-test.zip

image

Corner cases

Dealing with record privileges

if v3 revisionOf v2 revisionOf v1 and v2 is not public, versions will not list v2 if anonymous but the relationship will be resolved. It is recommended to have all versions in the same publication status.

Remote records

The catalogue allows to link to remote records using a URL. Those are ignored in the version resolution (because it would require to have parsable record where revision can be extracted which is usually not the case. Most of remote records link are pointing to HTML document).

Related work

Checklist

  • I have read the contribution guidelines
  • Pull request provided for main branch, backports managed with label
  • Good housekeeping of code, cleaning up comments, tests, and documentation
  • Clean commit history broken into understandable chucks, avoiding big commits with hundreds of files, cautious of reformatting and whitespace changes
  • Clean commit messages, longer verbose messages are encouraged
  • API Changes are identified in commit messages
  • Testing provided for features or enhancements using automatic tests
  • User documentation provided for new features or enhancements in manual
  • Build documentation provided for development instructions in README.md files
  • Library management using pom.xml dependency management. Update build documentation with intended library use and library tutorials or documentation

Funded by Ifremer

@fxprunayre fxprunayre added this to the 4.4.11 milestone Apr 3, 2026
@fxprunayre fxprunayre added api change Indicate a change in the API changelog labels Apr 3, 2026
Quite often dataset are updated on a regular basis and new versions are released and past ones are flagged as superseded/deprecated/archived. Some users have a need of being able to get an overview of all versions of a datasets.

So far, one way to get that overview was to group all versions in a collection but that adds extra work to encode and organize the collection properly eg. https://sdi.eea.europa.eu/catalogue/srv/eng/catalog.search#/metadata/3ccf2fe3-40e3-4968-a54c-5a59510fa5a3

The catalogue is also able to navigate to the next revision of record
when a `revisionOf` association is created between 2 records.

The main goal is to be able to list for a dataset all versions and more easily navigate to next/previous versions using the API.

The API provide an endpoint for retrieving verions:
* GET `/records/{metadataUuid}/associated?type=versions` returns all past and more recent versions
* GET `/records/{metadataUuid}/associated?type=next`
* GET `/records/{metadataUuid}/associated?type=previous`

Those associations can also be retrieved when searching using
`relatedType` parameter.

In ISO19115-3, associated resource can link to revision using the association type `revisionOf` link. Encoding is made using:

```xml
<mri:associatedResource>
  <mri:MD_AssociatedResource>
    <mri:associationType>
      <mri:DS_AssociationTypeCode codeList="http://standards.iso.org/iso/19115/resources/Codelists/cat/codelists.xml#DS_AssociationTypeCode" codeListValue="revisionOf"/>
    </mri:associationType>
    <mri:metadataReference uuidref="65d546c6-3577-4938-8322-5d95188779a1"/>
  </mri:MD_AssociatedResource>
</mri:associatedResource>
```

In the index, this information is stored in `agg_associated_revisionOf`.

if v3 revisionOf v2 revisionOf v1 and v2 is not public, versions will not list v2 if anonymous but the relationship will be resolved. It is recommended to have all versions in the same publication status.

The catalogue allows to link to remote records using a URL. Those are
ignored in the version resolution (because it would require to have
parsable record where revision can be extracted which is usually not the
case). Most of remote records link are pointing to HTML document.
Add specific layout in related component to display the list of versions
with:
* link to each version
* edition in any
* publication date if any
* status if any

The version list is added to the lineage section.
@fxprunayre fxprunayre force-pushed the 44-record-versions-list branch from f1be5ff to 5239c62 Compare April 3, 2026 10:32
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Comment on lines +122 to +123
"- `next`: immediate next item in version ordering.\n" +
"- `previous`: immediate previous item in version ordering.\n" +
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names are fairly generic and, in theory, could be used for any type of relationship that can be paginated. But, are far as are documented, I suppose they’re fine.

for (AssociatedRecord record : entry.getValue()) {
Element relation = new Element(entry.getKey().name());
relation.setAttribute("uuid", record.getUuid());
relation.setAttribute("uuid", record.getUuid() == null ? "" : record.getUuid());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the check for null to return an empty string can be added directly to AssociatedRecord.getUuid, so any place using it, doesn't get null value?

}

Map<String, AssociatedRecord> recordsByUuid = new LinkedHashMap<>();
for (AssociatedRecord record : records) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the Sonarcloud suggestion to rename record as it is a restricted identifier.


List<AssociatedRecord> reordered = new ArrayList<>();
for (String uuid : orderedRecords) {
AssociatedRecord record = recordsByUuid.remove(uuid);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the Sonarcloud suggestion to rename record as it is a restricted identifier.

Comment on lines +629 to +632
String uuid = String.valueOf(value).trim();
if (StringUtils.isNotEmpty(uuid)) {
values.add(uuid);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
String uuid = String.valueOf(value).trim();
if (StringUtils.isNotEmpty(uuid)) {
values.add(uuid);
}
String textValue = String.valueOf(value).trim();
if (StringUtils.isNotEmpty(textValue)) {
values.add(textValue);
}

This seems a generic method, uuid doesn't seem a good name.

.andExpect(jsonPath("$.versions[1]._source.uuid").value(VERSION_MIDDLE))
.andExpect(jsonPath("$.versions[2]._source.uuid").value(VERSION_OLDEST));

mockMvc.perform(get("/srv/api/records/" + VERSION_OLDEST + "/associated?type=versions")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be checked also VERSION_MIDDLE?

RelatedTypeDetails versionDetails, String currentUuid, boolean isNext) {
List<String> orderedRecords = versionDetails.getOrderedRecords();

if (orderedRecords == null || orderedRecords.isEmpty()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be really null?

result);
}

private static Set<String> extractFieldValues(Object fieldValue) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If EsSearchManager.getDocument would return a class IndexRecord class wrapping Map<String, Object>, the methods extractFieldValues, addFieldValue could be added there, improving cohesion, but that will require quite some changes and probably out of the scope of the PR.

* Remote records are not supported because they may not be XML document
* so we can't assume that we will be able to extract revisionOf records.
*/
private static RelatedTypeDetails getAllVersions(EsSearchManager searchMan, String metadataUuid) throws Exception {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried because I'm not sure whether circular relationships can be created that might lead to infinite loops.

Perhaps it can't happen, but I'm not sure.

/**
* For versions, order is based on the version graph.
*/
private static List<AssociatedRecord> reorderIndexDocBasedOnOrderedRecords(List<AssociatedRecord> records, List<String> orderedRecords) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be a good idea to move all these methods related to retrieving versions into a custom class called MetadataVersions or MetadataRelatedVersions or something similar. This would make the code cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Indicate a change in the API changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants