From e7edab511186dda0346f41a264d49eed515788de Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:14:48 +0100 Subject: [PATCH 01/10] extract logic from getMetaDataChildren into reusable getMetaDataChildrenResponse --- .../metadata/GenericController.java | 129 ++++++++++-------- 1 file changed, 75 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java index 587993de9..2e79a7a75 100644 --- a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java +++ b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java @@ -61,6 +61,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Stream; import static java.lang.String.format; import static org.fairdatapoint.util.HttpUtil.*; @@ -305,10 +306,22 @@ public ResponseEntity getMetaDataChildren( @RequestParam(defaultValue = "0") final int page, @RequestParam(defaultValue = "10") final int size ) throws MetadataServiceException, MetadataRepositoryException { - // 1. Init - final Model resultRdf = new LinkedHashModel(); + final String urlPrefix = oUrlPrefix.orElse(""); final String recordId = oRecordId.orElse(""); + return getMetaDataChildrenResponse(urlPrefix, recordId, childPrefix, Optional.of(page), Optional.of(size)); + } + + private ResponseEntity getMetaDataChildrenResponse( + final String urlPrefix, + final String recordId, + final String childPrefix, + Optional oPage, + Optional oSize + ) throws MetadataServiceException, MetadataRepositoryException { + // 1. Init + final HttpHeaders responseHeaders = new HttpHeaders(); + final Model resultRdf = new LinkedHashModel(); final MetadataService metadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(urlPrefix); // 2. Get entity (from repository based on permissions) @@ -317,64 +330,72 @@ public ResponseEntity getMetaDataChildren( final RepositoryMode mode = oCurrentUser.isEmpty() ? RepositoryMode.MAIN : RepositoryMode.COMBINED; final Model entity = metadataService.retrieve(entityUri, mode); - // 3. Get Children + // 3. Get requested resource definition relation final ResourceDefinition rd = resourceDefinitionService.getByUrlPrefix(urlPrefix); final ResourceDefinition currentChildRd = resourceDefinitionService.getByUrlPrefix(childPrefix); final MetadataService childMetadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(childPrefix); + // todo: rename ResourceDefinitionChild and related variables as part of #821 + final Optional optionalChild = rd.getChildren().stream() + .filter(child -> child.getTarget().getUuid().equals(currentChildRd.getUuid())) + .findFirst(); - for (ResourceDefinitionChild rdChild : rd.getChildren()) { - if (rdChild.getTarget().getUuid().equals(currentChildRd.getUuid())) { - final IRI relationUri = i(rdChild.getRelationUri()); - - // 3.1 Get all titles for sort - final Map titles = - metadataRepository.findChildTitles(entityUri, relationUri, RepositoryMode.COMBINED); - - // 3.2 Get all children sorted - final List children = getObjectsBy(entity, entityUri, relationUri) - .stream() - .filter(childUri -> getResourceNameForChild(childUri.toString()).equals(childPrefix)) - .filter(childUri -> { - try { - return oCurrentUser.isPresent() - || metadataStateService.isPublished(i(childUri.stringValue())); - } - catch (MetadataServiceException exc) { - return false; - } - }) - .sorted((value1, value2) -> { - final String title1 = titles.get(value1.toString()); - final String title2 = titles.get(value2.toString()); - if (title1 == null) { - return -1; - } - if (title2 == null) { - return 1; - } - return title1.compareToIgnoreCase(title2); - }) - .toList(); - - // 3.3 Retrieve children metadata only for requested page - final int childrenCount = children.size(); - children.stream().skip((long) page * size).limit(size) - .map(childUri -> retrieveChildModel(childMetadataService, childUri)) - .flatMap(Optional::stream) - .forEach(resultRdf::addAll); - - // 3.4 Set Link headers and send response - final HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.set( - "Link", - createLinkHeader(entityUri.stringValue(), childPrefix, childrenCount, page, size) - ); - return ResponseEntity.ok().headers(responseHeaders).body(resultRdf); - } + // Send empty response in case nothing was found + if (optionalChild.isEmpty()) { + return ResponseEntity.ok().body(resultRdf); } - // Send empty response in case nothing was found - return ResponseEntity.ok(resultRdf); + // 4. Get metadata of children + final ResourceDefinitionChild rdChild = optionalChild.get(); + final IRI relationUri = i(rdChild.getRelationUri()); + + // 4.1 Get all titles for sort + final Map titles = + metadataRepository.findChildTitles(entityUri, relationUri, RepositoryMode.COMBINED); + + // 4.2 Get all children sorted + final List children = getObjectsBy(entity, entityUri, relationUri) + .stream() + .filter(childUri -> getResourceNameForChild(childUri.toString()).equals(childPrefix)) + .filter(childUri -> { + try { + return oCurrentUser.isPresent() + || metadataStateService.isPublished(i(childUri.stringValue())); + } + catch (MetadataServiceException exc) { + return false; + } + }) + .sorted((value1, value2) -> { + final String title1 = titles.get(value1.toString()); + final String title2 = titles.get(value2.toString()); + if (title1 == null) { + return -1; + } + if (title2 == null) { + return 1; + } + return title1.compareToIgnoreCase(title2); + }) + .toList(); + + // 4.3 Limit children to requested page size + final int page = oPage.orElse(0); + Stream childrenStream = children.stream(); + if (oSize.isPresent()) { + // use paging + final int size = oSize.get(); + childrenStream = children.stream().skip((long) page * size).limit(size); + responseHeaders.set( + "Link", createLinkHeader(entityUri.stringValue(), childPrefix, children.size(), page, size) + ); + } + + // 4.4 Get metadata for selected children + childrenStream.map(childUri -> retrieveChildModel(childMetadataService, childUri)) + .flatMap(Optional::stream) + .forEach(resultRdf::addAll); + + return ResponseEntity.ok().headers(responseHeaders).body(resultRdf); } private String getResourceNameForChild(String url) { From fe0d951dc04c7bd1c58240c34f8e16fd3d86d05c Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:03:02 +0100 Subject: [PATCH 02/10] add separate controller for GET *// This has optional paging, instead of the required paging implemented for the explicit */page/ controller. --- .../controller/metadata/GenericController.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java index 2e79a7a75..66acbdadb 100644 --- a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java +++ b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java @@ -294,6 +294,23 @@ public ResponseEntity deleteMetadata( return ResponseEntity.noContent().build(); } + @Operation(hidden = true) + @GetMapping( + path = {"{childPrefix}/", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/{childPrefix}/"}, + produces = "!application/json" + ) + public ResponseEntity getMetaDataChildrenContainer( + @PathVariable final Optional oUrlPrefix, + @PathVariable final Optional oRecordId, + @PathVariable final String childPrefix, + @RequestParam final Optional page, + @RequestParam final Optional size + ) throws MetadataServiceException, MetadataRepositoryException { + final String urlPrefix = oUrlPrefix.orElse(""); + final String recordId = oRecordId.orElse(""); + return getMetaDataChildrenResponse(urlPrefix, recordId, childPrefix, page, size); + } + @Operation(hidden = true) @GetMapping( path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"}, @@ -306,7 +323,6 @@ public ResponseEntity getMetaDataChildren( @RequestParam(defaultValue = "0") final int page, @RequestParam(defaultValue = "10") final int size ) throws MetadataServiceException, MetadataRepositoryException { - final String urlPrefix = oUrlPrefix.orElse(""); final String recordId = oRecordId.orElse(""); return getMetaDataChildrenResponse(urlPrefix, recordId, childPrefix, Optional.of(page), Optional.of(size)); From 6c1b31ef70dc05405a4bfea9603be6821fabd02a Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:13:08 +0100 Subject: [PATCH 03/10] deprecate the */page/ controller in favor of the *// controller with optional paging --- .../api/controller/metadata/GenericController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java index 66acbdadb..53700d8a4 100644 --- a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java +++ b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java @@ -311,7 +311,7 @@ public ResponseEntity getMetaDataChildrenContainer( return getMetaDataChildrenResponse(urlPrefix, recordId, childPrefix, page, size); } - @Operation(hidden = true) + @Operation(hidden = true, deprecated = true) @GetMapping( path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"}, produces = "!application/json" From 17d4e4c21284d6818c524734a3ceee3c5ef5ce08 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:44:56 +0100 Subject: [PATCH 04/10] move logic from getMetaDataChildrenResponse into getMetaDataChildrenContainer --- .../metadata/GenericController.java | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java index 53700d8a4..3538fc691 100644 --- a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java +++ b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java @@ -300,41 +300,15 @@ public ResponseEntity deleteMetadata( produces = "!application/json" ) public ResponseEntity getMetaDataChildrenContainer( - @PathVariable final Optional oUrlPrefix, - @PathVariable final Optional oRecordId, @PathVariable final String childPrefix, - @RequestParam final Optional page, - @RequestParam final Optional size - ) throws MetadataServiceException, MetadataRepositoryException { - final String urlPrefix = oUrlPrefix.orElse(""); - final String recordId = oRecordId.orElse(""); - return getMetaDataChildrenResponse(urlPrefix, recordId, childPrefix, page, size); - } - - @Operation(hidden = true, deprecated = true) - @GetMapping( - path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"}, - produces = "!application/json" - ) - public ResponseEntity getMetaDataChildren( @PathVariable final Optional oUrlPrefix, @PathVariable final Optional oRecordId, - @PathVariable final String childPrefix, - @RequestParam(defaultValue = "0") final int page, - @RequestParam(defaultValue = "10") final int size + @RequestParam final Optional oPage, + @RequestParam final Optional oSize ) throws MetadataServiceException, MetadataRepositoryException { + // Defaults final String urlPrefix = oUrlPrefix.orElse(""); final String recordId = oRecordId.orElse(""); - return getMetaDataChildrenResponse(urlPrefix, recordId, childPrefix, Optional.of(page), Optional.of(size)); - } - - private ResponseEntity getMetaDataChildrenResponse( - final String urlPrefix, - final String recordId, - final String childPrefix, - Optional oPage, - Optional oSize - ) throws MetadataServiceException, MetadataRepositoryException { // 1. Init final HttpHeaders responseHeaders = new HttpHeaders(); final Model resultRdf = new LinkedHashModel(); @@ -414,6 +388,21 @@ private ResponseEntity getMetaDataChildrenResponse( return ResponseEntity.ok().headers(responseHeaders).body(resultRdf); } + @Operation(hidden = true, deprecated = true) + @GetMapping( + path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"}, + produces = "!application/json" + ) + public ResponseEntity getMetaDataChildren( + @PathVariable final Optional oUrlPrefix, + @PathVariable final Optional oRecordId, + @PathVariable final String childPrefix, + @RequestParam(defaultValue = "0") final int page, + @RequestParam(defaultValue = "10") final int size + ) throws MetadataServiceException, MetadataRepositoryException { + return getMetaDataChildrenContainer(childPrefix, oUrlPrefix, oRecordId, Optional.of(page), Optional.of(size)); + } + private String getResourceNameForChild(String url) { final String[] parts = url .replace(persistentUrl, "") From e3eca3d5f8899e571ca61d5780e96712c13f80f4 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:55:27 +0100 Subject: [PATCH 05/10] rename getMetaDataChildren controller methods for clarity --- .../api/controller/metadata/GenericController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java index 3538fc691..5f8fcc050 100644 --- a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java +++ b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java @@ -299,7 +299,7 @@ public ResponseEntity deleteMetadata( path = {"{childPrefix}/", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/{childPrefix}/"}, produces = "!application/json" ) - public ResponseEntity getMetaDataChildrenContainer( + public ResponseEntity getMetaDataChildren( @PathVariable final String childPrefix, @PathVariable final Optional oUrlPrefix, @PathVariable final Optional oRecordId, @@ -393,14 +393,14 @@ public ResponseEntity getMetaDataChildrenContainer( path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"}, produces = "!application/json" ) - public ResponseEntity getMetaDataChildren( + public ResponseEntity getMetaDataChildrenDeprecated( @PathVariable final Optional oUrlPrefix, @PathVariable final Optional oRecordId, @PathVariable final String childPrefix, @RequestParam(defaultValue = "0") final int page, @RequestParam(defaultValue = "10") final int size ) throws MetadataServiceException, MetadataRepositoryException { - return getMetaDataChildrenContainer(childPrefix, oUrlPrefix, oRecordId, Optional.of(page), Optional.of(size)); + return getMetaDataChildren(childPrefix, oUrlPrefix, oRecordId, Optional.of(page), Optional.of(size)); } private String getResourceNameForChild(String url) { From 42dde8a54360ae57d8462f25007fee9379e59ad8 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:55:36 +0100 Subject: [PATCH 06/10] move logic from getMetaDataChildren controller to new ContainerService --- .../metadata/GenericController.java | 80 +++---------- .../metadata/container/ContainerService.java | 106 ++++++++++++++++++ 2 files changed, 121 insertions(+), 65 deletions(-) create mode 100644 src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java diff --git a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java index 5f8fcc050..19021ac90 100644 --- a/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java +++ b/src/main/java/org/fairdatapoint/api/controller/metadata/GenericController.java @@ -28,17 +28,15 @@ import org.fairdatapoint.database.rdf.repository.RepositoryMode; import jakarta.servlet.http.HttpServletResponse; import org.fairdatapoint.database.rdf.repository.exception.MetadataRepositoryException; -import org.fairdatapoint.database.rdf.repository.generic.GenericMetadataRepository; import org.fairdatapoint.entity.exception.ForbiddenException; -import org.fairdatapoint.entity.exception.ValidationException; import org.fairdatapoint.entity.resource.ResourceDefinition; import org.fairdatapoint.entity.resource.ResourceDefinitionChild; import org.fairdatapoint.entity.user.UserAccount; import org.fairdatapoint.service.metadata.common.MetadataService; +import org.fairdatapoint.service.metadata.container.ContainerService; import org.fairdatapoint.service.metadata.enhance.MetadataEnhancer; import org.fairdatapoint.service.metadata.exception.MetadataServiceException; import org.fairdatapoint.service.metadata.factory.MetadataServiceFactory; -import org.fairdatapoint.service.metadata.state.MetadataStateService; import org.fairdatapoint.service.resource.ResourceDefinitionService; import org.fairdatapoint.service.schema.MetadataSchemaService; import org.fairdatapoint.service.search.SearchFilterCache; @@ -59,7 +57,6 @@ import java.net.URI; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -86,11 +83,9 @@ public class GenericController { private final MetadataEnhancer metadataEnhancer; - private final CurrentUserService currentUserService; - - private final GenericMetadataRepository metadataRepository; + private final ContainerService containerService; - private final MetadataStateService metadataStateService; + private final CurrentUserService currentUserService; private final SearchFilterCache searchFilterCache; @@ -309,18 +304,16 @@ public ResponseEntity getMetaDataChildren( // Defaults final String urlPrefix = oUrlPrefix.orElse(""); final String recordId = oRecordId.orElse(""); - // 1. Init + + // Init final HttpHeaders responseHeaders = new HttpHeaders(); final Model resultRdf = new LinkedHashModel(); - final MetadataService metadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(urlPrefix); - // 2. Get entity (from repository based on permissions) + // Get entity (from repository based on permissions) final Optional oCurrentUser = currentUserService.getCurrentUser(); - final IRI entityUri = getMetadataIRI(persistentUrl, urlPrefix, recordId); final RepositoryMode mode = oCurrentUser.isEmpty() ? RepositoryMode.MAIN : RepositoryMode.COMBINED; - final Model entity = metadataService.retrieve(entityUri, mode); - // 3. Get requested resource definition relation + // Get requested resource definition relation final ResourceDefinition rd = resourceDefinitionService.getByUrlPrefix(urlPrefix); final ResourceDefinition currentChildRd = resourceDefinitionService.getByUrlPrefix(childPrefix); final MetadataService childMetadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(childPrefix); @@ -329,46 +322,20 @@ public ResponseEntity getMetaDataChildren( .filter(child -> child.getTarget().getUuid().equals(currentChildRd.getUuid())) .findFirst(); - // Send empty response in case nothing was found + // Return empty response in case nothing was found if (optionalChild.isEmpty()) { return ResponseEntity.ok().body(resultRdf); } - // 4. Get metadata of children + // Get metadata of children final ResourceDefinitionChild rdChild = optionalChild.get(); + final IRI entityUri = getMetadataIRI(persistentUrl, urlPrefix, recordId); final IRI relationUri = i(rdChild.getRelationUri()); + final List children = containerService.getContainedValues( + childPrefix, urlPrefix, entityUri, relationUri, mode + ); - // 4.1 Get all titles for sort - final Map titles = - metadataRepository.findChildTitles(entityUri, relationUri, RepositoryMode.COMBINED); - - // 4.2 Get all children sorted - final List children = getObjectsBy(entity, entityUri, relationUri) - .stream() - .filter(childUri -> getResourceNameForChild(childUri.toString()).equals(childPrefix)) - .filter(childUri -> { - try { - return oCurrentUser.isPresent() - || metadataStateService.isPublished(i(childUri.stringValue())); - } - catch (MetadataServiceException exc) { - return false; - } - }) - .sorted((value1, value2) -> { - final String title1 = titles.get(value1.toString()); - final String title2 = titles.get(value2.toString()); - if (title1 == null) { - return -1; - } - if (title2 == null) { - return 1; - } - return title1.compareToIgnoreCase(title2); - }) - .toList(); - - // 4.3 Limit children to requested page size + // Limit children to requested page size final int page = oPage.orElse(0); Stream childrenStream = children.stream(); if (oSize.isPresent()) { @@ -380,7 +347,7 @@ public ResponseEntity getMetaDataChildren( ); } - // 4.4 Get metadata for selected children + // Get metadata for selected children childrenStream.map(childUri -> retrieveChildModel(childMetadataService, childUri)) .flatMap(Optional::stream) .forEach(resultRdf::addAll); @@ -403,23 +370,6 @@ public ResponseEntity getMetaDataChildrenDeprecated( return getMetaDataChildren(childPrefix, oUrlPrefix, oRecordId, Optional.of(page), Optional.of(size)); } - private String getResourceNameForChild(String url) { - final String[] parts = url - .replace(persistentUrl, "") - .split("/"); - - if (parts.length < 2) { - throw new ValidationException("Unsupported URL"); - } - - // If URL is a repository -> return empty string - if (parts[1].equals("page")) { - return ""; - } - - return parts[1]; - } - private String createLinkHeader(String entityUrl, String childPrefix, int childrenCount, int page, int size) { final List links = new LinkedList(); final int lastPage = (int) Math.ceil((float) childrenCount / size) - 1; diff --git a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java new file mode 100644 index 000000000..313a34acf --- /dev/null +++ b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java @@ -0,0 +1,106 @@ +package org.fairdatapoint.service.metadata.container; + +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Model; +import org.eclipse.rdf4j.model.Value; +import org.fairdatapoint.database.rdf.repository.RepositoryMode; +import org.fairdatapoint.database.rdf.repository.exception.MetadataRepositoryException; +import org.fairdatapoint.database.rdf.repository.generic.GenericMetadataRepository; +import org.fairdatapoint.entity.exception.ValidationException; +import org.fairdatapoint.service.metadata.common.MetadataService; +import org.fairdatapoint.service.metadata.exception.MetadataServiceException; +import org.fairdatapoint.service.metadata.factory.MetadataServiceFactory; +import org.fairdatapoint.service.metadata.state.MetadataStateService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; + +import static org.fairdatapoint.util.RdfUtil.getObjectsBy; +import static org.fairdatapoint.util.ValueFactoryHelper.i; + +@Service +public class ContainerService { + + private final GenericMetadataRepository metadataRepository; + + private final MetadataServiceFactory metadataServiceFactory; + + private final MetadataStateService metadataStateService; + + private final String persistentUrl; + + /** + * Constructor (autowiring) + */ + public ContainerService( + GenericMetadataRepository metadataRepository, + MetadataServiceFactory metadataServiceFactory, + MetadataStateService metadataStateService, String persistentUrl + ) { + this.metadataRepository = metadataRepository; + this.metadataServiceFactory = metadataServiceFactory; + this.metadataStateService = metadataStateService; + this.persistentUrl = persistentUrl; + } + + public List getContainedValues( + String childPrefix, + String urlPrefix, + IRI entityUri, + IRI relationUri, + RepositoryMode repositoryMode + ) throws MetadataServiceException, MetadataRepositoryException { + + // 4.1 Get all titles for sort + final Map titles = metadataRepository.findChildTitles( + entityUri, relationUri, RepositoryMode.COMBINED); + + // 4.2 Get all children sorted + final MetadataService metadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(urlPrefix); + final Model entity = metadataService.retrieve(entityUri, repositoryMode); + + final List objects = getObjectsBy(entity, entityUri, relationUri); + return objects.stream() + .filter(childUri -> getResourceNameForChild(childUri.toString()).equals(childPrefix)) + // although entity may be published, it could contain URIs of draft resources + .filter(childUri -> { + try { + return repositoryMode == RepositoryMode.COMBINED + || metadataStateService.isPublished(i(childUri.stringValue())); + } + catch (MetadataServiceException exc) { + return false; + } + }) + .sorted((value1, value2) -> { + final String title1 = titles.get(value1.toString()); + final String title2 = titles.get(value2.toString()); + if (title1 == null) { + return -1; + } + if (title2 == null) { + return 1; + } + return title1.compareToIgnoreCase(title2); + }) + .toList(); + } + + private String getResourceNameForChild(String url) { + final String[] parts = url + .replace(persistentUrl, "") + .split("/"); + + if (parts.length < 2) { + throw new ValidationException("Unsupported URL"); + } + + // If URL is a repository -> return empty string + if (parts[1].equals("page")) { + return ""; + } + + return parts[1]; + } +} From 2510ede67f16cf0c9b64cbb596521be8268f71e8 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:12:36 +0100 Subject: [PATCH 07/10] replace custom anonymous comparator by Comparator.comparing --- .../metadata/container/ContainerService.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java index 313a34acf..38c1fc879 100644 --- a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java +++ b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java @@ -13,6 +13,7 @@ import org.fairdatapoint.service.metadata.state.MetadataStateService; import org.springframework.stereotype.Service; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -73,17 +74,11 @@ public List getContainedValues( return false; } }) - .sorted((value1, value2) -> { - final String title1 = titles.get(value1.toString()); - final String title2 = titles.get(value2.toString()); - if (title1 == null) { - return -1; - } - if (title2 == null) { - return 1; - } - return title1.compareToIgnoreCase(title2); - }) + // todo: verify if Comparator implementation is equivalent to the original + .sorted(Comparator.comparing( + childUri -> titles.get(childUri.toString()), + String.CASE_INSENSITIVE_ORDER) + ) .toList(); } From 03c669b5f20172ac26ec5b7479f518e05b64610e Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:13:04 +0100 Subject: [PATCH 08/10] rename objects to values redundant variable assignment instead of returning directly, but this helps with clarity and debugging with todos --- .../service/metadata/container/ContainerService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java index 38c1fc879..a41659bf1 100644 --- a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java +++ b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java @@ -61,8 +61,11 @@ public List getContainedValues( final MetadataService metadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(urlPrefix); final Model entity = metadataService.retrieve(entityUri, repositoryMode); - final List objects = getObjectsBy(entity, entityUri, relationUri); - return objects.stream() + // https://rdf4j.org/javadoc/latest/org/eclipse/rdf4j/model/Value.html + final List values = getObjectsBy(entity, entityUri, relationUri); + return values.stream() + // todo: can we replace Value by MemIRI? + // todo: this uses both Value.toString() and Value.stringValue(). should we only use tha latter? .filter(childUri -> getResourceNameForChild(childUri.toString()).equals(childPrefix)) // although entity may be published, it could contain URIs of draft resources .filter(childUri -> { From 99f46eaea3efe581f106fadcb02f12889c6e5355 Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:12:06 +0100 Subject: [PATCH 09/10] add license to ContainerService.java --- .../metadata/container/ContainerService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java index a41659bf1..7599ac5b8 100644 --- a/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java +++ b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java @@ -1,3 +1,25 @@ +/** + * The MIT License + * Copyright © 2016-2024 FAIR Data Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.fairdatapoint.service.metadata.container; import org.eclipse.rdf4j.model.IRI; From 7a8bb02e221fe4bc192871e32993ebebafc6d01e Mon Sep 17 00:00:00 2001 From: dennisvang <29799340+dennisvang@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:13:31 +0100 Subject: [PATCH 10/10] add ContainerServiceTest testing comparator equality This shows that the two implementations are *not* identical in the way they handle null values... --- .../container/ContainerServiceTest.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/test/java/org/fairdatapoint/service/metadata/container/ContainerServiceTest.java diff --git a/src/test/java/org/fairdatapoint/service/metadata/container/ContainerServiceTest.java b/src/test/java/org/fairdatapoint/service/metadata/container/ContainerServiceTest.java new file mode 100644 index 000000000..568d204f0 --- /dev/null +++ b/src/test/java/org/fairdatapoint/service/metadata/container/ContainerServiceTest.java @@ -0,0 +1,43 @@ +package org.fairdatapoint.service.metadata.container; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Comparator; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ContainerServiceTest { + + final private Comparator newComparator = Comparator.comparing( + String::valueOf, String.CASE_INSENSITIVE_ORDER + ); + + private int oldCompare(String title1, String title2) { + if (title1 == null) { + return -1; + } + if (title2 == null) { + return 1; + } + return title1.compareToIgnoreCase(title2); + } + + private int newCompare(String title1, String title2) { + return newComparator.compare(title1, title2); + } + + /** + * The original GenericController.getMetaDataChildren used a custom comparator for sorting, + * reproduced as in the oldCompare method above. + * Also see api/controller/metadata/GenericController.java L346 (de41d47). + * This is now replaced by Comparator.comparing(), like newComparator above, + * so we need to verify that the resulting outcome is identical. + */ + @ParameterizedTest + @CsvSource({",", "foo,", ",bar", "foo,bar", "foo,foo", "foo,FOO", "BAR,bar"}) + public void testContainerServiceComparatorEquality(String string1, String string2) { + assertEquals(oldCompare(string1, string2), newCompare(string1, string2)); + } +}