Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.*;
Expand All @@ -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;

Expand Down Expand Up @@ -295,103 +291,83 @@ public ResponseEntity<Void> deleteMetadata(

@Operation(hidden = true)
@GetMapping(
path = {"page/{childPrefix}", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/page/{childPrefix}"},
path = {"{childPrefix}/", "{oUrlPrefix:[^.]+}/{oRecordId:[^.]+}/{childPrefix}/"},
produces = "!application/json"
)
public ResponseEntity<Model> getMetaDataChildren(
@PathVariable final String childPrefix,
@PathVariable final Optional<String> oUrlPrefix,
@PathVariable final Optional<String> oRecordId,
@PathVariable final String childPrefix,
@RequestParam(defaultValue = "0") final int page,
@RequestParam(defaultValue = "10") final int size
@RequestParam final Optional<Integer> oPage,
@RequestParam final Optional<Integer> 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<UserAccount> 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<String, String> titles =
metadataRepository.findChildTitles(entityUri, relationUri, RepositoryMode.COMBINED);

// 3.2 Get all children sorted
final List<Value> 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<ResourceDefinitionChild> 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<Value> children = containerService.getContainedValues(
childPrefix, urlPrefix, entityUri, relationUri, mode
);

// Limit children to requested page size
final int page = oPage.orElse(0);
Stream<Value> 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<Model> getMetaDataChildrenDeprecated(
@PathVariable final Optional<String> oUrlPrefix,
@PathVariable final Optional<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Value> getContainedValues(
String childPrefix,
String urlPrefix,
IRI entityUri,
IRI relationUri,
RepositoryMode repositoryMode
) throws MetadataServiceException, MetadataRepositoryException {

// 4.1 Get all titles for sort
final Map<String, String> 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<Value> 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];
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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));
}
}
Loading