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..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,8 +57,8 @@ 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; import static java.lang.String.format; import static org.fairdatapoint.util.HttpUtil.*; @@ -85,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; @@ -295,103 +291,83 @@ public ResponseEntity deleteMetadata( @Operation(hidden = true) @GetMapping( - path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"}, + path = {"{childPrefix}/", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/{childPrefix}/"}, produces = "!application/json" ) public ResponseEntity getMetaDataChildren( + @PathVariable final String childPrefix, @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 { - // 1. Init - final Model resultRdf = new LinkedHashModel(); + // Defaults final String urlPrefix = oUrlPrefix.orElse(""); final String recordId = oRecordId.orElse(""); - final MetadataService metadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(urlPrefix); - // 2. Get entity (from repository based on permissions) + // Init + final HttpHeaders responseHeaders = new HttpHeaders(); + final Model resultRdf = new LinkedHashModel(); + + // 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 Children + // Get requested resource definition relation final ResourceDefinition rd = resourceDefinitionService.getByUrlPrefix(urlPrefix); final ResourceDefinition currentChildRd = resourceDefinitionService.getByUrlPrefix(childPrefix); final MetadataService childMetadataService = metadataServiceFactory.getMetadataServiceByUrlPrefix(childPrefix); - - 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); - } + // 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(); + + // Return 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); - } - - private String getResourceNameForChild(String url) { - final String[] parts = url - .replace(persistentUrl, "") - .split("/"); - - if (parts.length < 2) { - throw new ValidationException("Unsupported URL"); + // 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 + ); + + // 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) + ); } - // If URL is a repository -> return empty string - if (parts[1].equals("page")) { - return ""; - } + // Get metadata for selected children + childrenStream.map(childUri -> retrieveChildModel(childMetadataService, childUri)) + .flatMap(Optional::stream) + .forEach(resultRdf::addAll); - return parts[1]; + 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 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 getMetaDataChildren(childPrefix, oUrlPrefix, oRecordId, Optional.of(page), Optional.of(size)); } private String createLinkHeader(String entityUrl, String childPrefix, int childrenCount, int page, int size) { 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..7599ac5b8 --- /dev/null +++ b/src/main/java/org/fairdatapoint/service/metadata/container/ContainerService.java @@ -0,0 +1,126 @@ +/** + * 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; +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.Comparator; +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); + + // 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 -> { + try { + return repositoryMode == RepositoryMode.COMBINED + || metadataStateService.isPublished(i(childUri.stringValue())); + } + catch (MetadataServiceException exc) { + return false; + } + }) + // todo: verify if Comparator implementation is equivalent to the original + .sorted(Comparator.comparing( + childUri -> titles.get(childUri.toString()), + String.CASE_INSENSITIVE_ORDER) + ) + .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]; + } +} 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)); + } +}