diff --git a/src/main/java/edu/kit/datamanager/pit/Application.java b/src/main/java/edu/kit/datamanager/pit/Application.java index 219118ac..379cea58 100644 --- a/src/main/java/edu/kit/datamanager/pit/Application.java +++ b/src/main/java/edu/kit/datamanager/pit/Application.java @@ -92,7 +92,7 @@ public class Application { protected static final String ERROR_COMMUNICATION = "Communication error: {}"; protected static final String ERROR_CONFIGURATION = "Configuration error: {}"; - protected static final Executor EXECUTOR = Executors.newWorkStealingPool(); + public static final Executor EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); @Bean diff --git a/src/main/java/edu/kit/datamanager/pit/common/RecordValidationException.java b/src/main/java/edu/kit/datamanager/pit/common/RecordValidationException.java index 520cc748..ab63aab0 100644 --- a/src/main/java/edu/kit/datamanager/pit/common/RecordValidationException.java +++ b/src/main/java/edu/kit/datamanager/pit/common/RecordValidationException.java @@ -28,6 +28,11 @@ public RecordValidationException(PIDRecord pidRecord, String reason) { this.pidRecord = pidRecord; } + public RecordValidationException(PIDRecord pidRecord, String reason, Exception e) { + super(HTTP_STATUS, VALIDATION_OF_RECORD + pidRecord.getPid() + " failed. Reason:\n" + reason, e); + this.pidRecord = pidRecord; + } + public PIDRecord getPidRecord() { return pidRecord; } diff --git a/src/main/java/edu/kit/datamanager/pit/configuration/ApplicationProperties.java b/src/main/java/edu/kit/datamanager/pit/configuration/ApplicationProperties.java index 6b313b03..6e206511 100644 --- a/src/main/java/edu/kit/datamanager/pit/configuration/ApplicationProperties.java +++ b/src/main/java/edu/kit/datamanager/pit/configuration/ApplicationProperties.java @@ -21,7 +21,10 @@ import edu.kit.datamanager.pit.pitservice.impl.NoValidationStrategy; import java.net.URL; +import java.util.List; +import java.util.Set; +import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; import jakarta.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Value; @@ -46,6 +49,11 @@ @Validated public class ApplicationProperties extends GenericApplicationProperties { + private static Set KNOWN_PROFILE_KEYS = Set.of( + "21.T11148/076759916209e5d62bd5", + "21.T11969/bcc54a2a9ab5bf2a8f2c" + ); + public enum IdentifierSystemImpl { IN_MEMORY, LOCAL, @@ -66,10 +74,10 @@ public enum ValidationStrategy { private ValidationStrategy validationStrategy = ValidationStrategy.EMBEDDED_STRICT; @Bean - public IValidationStrategy defaultValidationStrategy() { + public IValidationStrategy defaultValidationStrategy(ITypeRegistry typeRegistry) { IValidationStrategy defaultStrategy = new NoValidationStrategy(); if (this.validationStrategy == ValidationStrategy.EMBEDDED_STRICT) { - defaultStrategy = new EmbeddedStrictValidatorStrategy(); + defaultStrategy = new EmbeddedStrictValidatorStrategy(typeRegistry, this); } return defaultStrategy; } @@ -108,8 +116,24 @@ public boolean storesResolved() { private long expireAfterWrite; @Value("${pit.validation.profileKey:21.T11148/076759916209e5d62bd5}") + @Deprecated(forRemoval = true /*In Typed PID Maker 3.0.0*/) private String profileKey; + public @NotNull Set getProfileKeys() { + Set allProfileKeys = new java.util.HashSet<>(Set.copyOf(KNOWN_PROFILE_KEYS)); + allProfileKeys.addAll(profileKeys); + allProfileKeys.add(this.getProfileKey()); + return allProfileKeys; + } + + public void setProfileKeys(@NotNull List profileKeys) { + this.profileKeys = profileKeys; + } + + @Value("#{${pit.validation.profileKeys:{}}}") + @NotNull + protected List profileKeys = List.of(); + public IdentifierSystemImpl getIdentifierSystemImplementation() { return this.identifierSystemImplementation; } diff --git a/src/main/java/edu/kit/datamanager/pit/domain/ImmutableList.java b/src/main/java/edu/kit/datamanager/pit/domain/ImmutableList.java new file mode 100644 index 00000000..f2e2bd3a --- /dev/null +++ b/src/main/java/edu/kit/datamanager/pit/domain/ImmutableList.java @@ -0,0 +1,10 @@ +package edu.kit.datamanager.pit.domain; + +import java.util.Collections; +import java.util.List; + +public record ImmutableList(List items) { + public ImmutableList { + items = Collections.unmodifiableList(items); + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/pit/domain/PIDRecord.java b/src/main/java/edu/kit/datamanager/pit/domain/PIDRecord.java index 45629e31..c0d785ee 100644 --- a/src/main/java/edu/kit/datamanager/pit/domain/PIDRecord.java +++ b/src/main/java/edu/kit/datamanager/pit/domain/PIDRecord.java @@ -164,6 +164,7 @@ public void removeAllValuesOf(String attribute) { * @param profile the given Profile definition. * @return all missing mandatory attributes. */ + @Deprecated public Collection getMissingMandatoryTypesOf(TypeDefinition profile) { Collection missing = new ArrayList<>(); for (TypeDefinition td : profile.getSubTypes().values()) { diff --git a/src/main/java/edu/kit/datamanager/pit/pitservice/impl/EmbeddedStrictValidatorStrategy.java b/src/main/java/edu/kit/datamanager/pit/pitservice/impl/EmbeddedStrictValidatorStrategy.java index afc65d97..3f9680f6 100644 --- a/src/main/java/edu/kit/datamanager/pit/pitservice/impl/EmbeddedStrictValidatorStrategy.java +++ b/src/main/java/edu/kit/datamanager/pit/pitservice/impl/EmbeddedStrictValidatorStrategy.java @@ -1,23 +1,26 @@ package edu.kit.datamanager.pit.pitservice.impl; -import com.github.benmanes.caffeine.cache.AsyncLoadingCache; import edu.kit.datamanager.pit.common.ExternalServiceException; import edu.kit.datamanager.pit.common.RecordValidationException; import edu.kit.datamanager.pit.configuration.ApplicationProperties; import edu.kit.datamanager.pit.domain.PIDRecord; import edu.kit.datamanager.pit.domain.TypeDefinition; import edu.kit.datamanager.pit.pitservice.IValidationStrategy; +import edu.kit.datamanager.pit.typeregistry.AttributeInfo; +import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; +import edu.kit.datamanager.pit.typeregistry.RegisteredProfile; import edu.kit.datamanager.pit.util.TypeValidationUtils; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.*; import java.util.stream.Collectors; import org.apache.commons.lang3.stream.Streams; +import org.everit.json.schema.ValidationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; /** * Validates a PID record using embedded profile(s). @@ -31,33 +34,71 @@ public class EmbeddedStrictValidatorStrategy implements IValidationStrategy { private static final Logger LOG = LoggerFactory.getLogger(EmbeddedStrictValidatorStrategy.class); protected static final Executor EXECUTOR = Executors.newWorkStealingPool(); - @Autowired - public AsyncLoadingCache typeLoader; + protected ITypeRegistry typeRegistry; + // TODO store extracted information only + protected ApplicationProperties config; + protected boolean additionalAttributesAllowed; + protected Set profileKeys; - @Autowired - ApplicationProperties applicationProps; + public EmbeddedStrictValidatorStrategy(ITypeRegistry typeRegistry, ApplicationProperties config) { + this.typeRegistry = typeRegistry; + this.config = config; + } @Override public void validate(PIDRecord pidRecord) throws RecordValidationException, ExternalServiceException { - String profileKey = applicationProps.getProfileKey(); - if (!pidRecord.hasProperty(profileKey)) { + + if (pidRecord.getPropertyIdentifiers().stream().anyMatch(this.profileKeys::contains)) { + // TODO to support records without referenced profiles, we could consider skipping this... throw new RecordValidationException( pidRecord, - "Profile attribute not found. Expected key: " + profileKey); + "Profile attribute not found. Expected key: " + this.profileKeys); } - String[] profilePIDs = pidRecord.getPropertyValues(profileKey); + // For each attribute in record, resolve schema and check the value + List> attributeInfoFutures = pidRecord.getPropertyIdentifiers().stream() + .map(attributePid -> this.typeRegistry.queryAttributeInfo(attributePid)) + .map(attributeInfoFuture -> attributeInfoFuture.thenApply(attributeInfo -> { + for (String value : pidRecord.getPropertyValues(attributeInfo.pid())) { + try { + attributeInfo.jsonSchema().validate(value); + } catch (ValidationException e) { + throw new RecordValidationException( + pidRecord, + "Attribute %s has a non-complying value %s".formatted(attributeInfo.pid(), value), + e + ); + } + } + return attributeInfo; + })) + .map(attributeInfoFuture -> attributeInfoFuture.thenApply(attributeInfo -> { + boolean isProfile = this.profileKeys.contains(attributeInfo.pid()); + if (isProfile) { + Arrays.stream(pidRecord.getPropertyValues(attributeInfo.pid())) + .map(profilePid -> this.typeRegistry.queryAsProfile(profilePid)) + .forEach(registeredProfileFuture -> registeredProfileFuture.thenApply(registeredProfile -> { + registeredProfile.validateAttributes(pidRecord, this.additionalAttributesAllowed); + return registeredProfile; + })); + } + })) + .toList(); + + CompletableFuture.allOf(attributeInfoFutures.toArray(new CompletableFuture[0])).join(); + + String[] profilePIDs = pidRecord.getPropertyValues(this.profileKeys); boolean hasProfile = profilePIDs.length > 0; if (!hasProfile) { throw new RecordValidationException( pidRecord, - "Profile attribute " + profileKey + " has no values."); + "Profile attribute " + this.profileKeys + " has no values."); } - List> futures = Streams.stream(Arrays.stream(profilePIDs)) + List> futures = Streams.failableStream(Arrays.stream(profilePIDs)) .map(profilePID -> { try { - return this.typeLoader.get(profilePID) + return this.typeRegistry.queryAsProfile(profilePID) .thenAcceptAsync(profileDefinition -> { if (profileDefinition == null) { LOG.error("No type definition found for identifier {}.", profilePID); @@ -70,7 +111,7 @@ public void validate(PIDRecord pidRecord) throws RecordValidationException, Exte } catch (RuntimeException e) { LOG.error("Could not resolve identifier {}.", profilePID); throw new ExternalServiceException( - applicationProps.getTypeRegistryUri().toString()); + this.typeRegistry.getRegistryUrl().toString()); } }) .collect(Collectors.toList()); @@ -79,7 +120,7 @@ public void validate(PIDRecord pidRecord) throws RecordValidationException, Exte } catch (CompletionException e) { throwRecordValidationExceptionCause(e); throw new ExternalServiceException( - applicationProps.getTypeRegistryUri().toString()); + this.typeRegistry.getRegistryUrl().toString()); } catch (CancellationException e) { throwRecordValidationExceptionCause(e); throw new RecordValidationException( @@ -101,7 +142,7 @@ private static void throwRecordValidationExceptionCause(Throwable e) { * @param profile the profile to validate against. * @throws RecordValidationException with error message on validation errors. */ - private void strictProfileValidation(PIDRecord pidRecord, TypeDefinition profile) throws RecordValidationException { + private void strictProfileValidation(PIDRecord pidRecord, RegisteredProfile profile) throws RecordValidationException { // if (profile.hasSchema()) { // TODO issue https://github.com/kit-data-manager/pit-service/issues/104 // validate using schema and you are done (strict validation) @@ -109,14 +150,14 @@ private void strictProfileValidation(PIDRecord pidRecord, TypeDefinition profile // return profile.validate(jsonRecord); // } - LOG.trace("Validating PID record against profile {}.", profile.getIdentifier()); + LOG.trace("Validating PID record against profile {}.", profile.pid()); TypeValidationUtils.checkMandatoryAttributes(pidRecord, profile); for (String attributeKey : pidRecord.getPropertyIdentifiers()) { LOG.trace("Checking PID record key {}.", attributeKey); - TypeDefinition type = profile.getSubTypes().get(attributeKey); + TypeDefinition type = profile.attributes().items().get(attributeKey); if (type == null) { LOG.error("No sub-type found for key {}.", attributeKey); // TODO try to resolve it (for later when we support "allow additional diff --git a/src/main/java/edu/kit/datamanager/pit/typeregistry/AttributeInfo.java b/src/main/java/edu/kit/datamanager/pit/typeregistry/AttributeInfo.java new file mode 100644 index 00000000..3d7f198e --- /dev/null +++ b/src/main/java/edu/kit/datamanager/pit/typeregistry/AttributeInfo.java @@ -0,0 +1,17 @@ +package edu.kit.datamanager.pit.typeregistry; + +import org.everit.json.schema.Schema; + +import java.util.List; + +/** + * @param pid the pid of this attribute + * @param typeName name of the schema type of this attribute in the DTR, + * e.g. "Profile", "InfoType", "Special-Info-Type", ... + * @param jsonSchema the json schema to validate a value of this attribute + */ +public record AttributeInfo( + String pid, + String typeName, + Schema jsonSchema +) {} diff --git a/src/main/java/edu/kit/datamanager/pit/typeregistry/ITypeRegistry.java b/src/main/java/edu/kit/datamanager/pit/typeregistry/ITypeRegistry.java index d5932e83..be6d0272 100644 --- a/src/main/java/edu/kit/datamanager/pit/typeregistry/ITypeRegistry.java +++ b/src/main/java/edu/kit/datamanager/pit/typeregistry/ITypeRegistry.java @@ -1,10 +1,9 @@ package edu.kit.datamanager.pit.typeregistry; -import java.io.IOException; +import org.everit.json.schema.Schema; -import edu.kit.datamanager.pit.domain.TypeDefinition; - -import java.net.URISyntaxException; +import java.net.URL; +import java.util.concurrent.CompletableFuture; /** * Main abstraction interface towards the type registry. Contains all methods @@ -12,13 +11,9 @@ * */ public interface ITypeRegistry { - - /** - * Queries a type definition record from the type registry. - * - * @param typeIdentifier - * @return a type definition record or null if the type is not registered. - * @throws IOException on communication errors with a remote registry - */ - public TypeDefinition queryTypeDefinition(String typeIdentifier) throws IOException, URISyntaxException; + @Deprecated + CompletableFuture queryAttributeSchemaOf(String attributePid); + CompletableFuture queryAttributeInfo(String attributePid); + CompletableFuture queryAsProfile(String profilePid); + URL getRegistryUrl(); } diff --git a/src/main/java/edu/kit/datamanager/pit/typeregistry/RegisteredProfile.java b/src/main/java/edu/kit/datamanager/pit/typeregistry/RegisteredProfile.java new file mode 100644 index 00000000..f9fbacd7 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/pit/typeregistry/RegisteredProfile.java @@ -0,0 +1,50 @@ +package edu.kit.datamanager.pit.typeregistry; + +import edu.kit.datamanager.pit.common.RecordValidationException; +import edu.kit.datamanager.pit.domain.ImmutableList; +import edu.kit.datamanager.pit.domain.PIDRecord; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public record RegisteredProfile( + String pid, + ImmutableList attributes +) { + public void validateAttributes(PIDRecord pidRecord, boolean allowAdditionalAttributes) { + Set additionalAttributes = pidRecord.getPropertyIdentifiers().stream() + .filter(recordKey -> attributes.items().stream().anyMatch( + profileAttribute -> Objects.equals(profileAttribute.pid(), recordKey))) + .collect(Collectors.toSet()); + + boolean violatesAdditionalAttributes = !allowAdditionalAttributes && !additionalAttributes.isEmpty(); + if (violatesAdditionalAttributes) { + throw new RecordValidationException( + pidRecord, + String.format("Attributes %s are not allowed in profile %s", + String.join(", ", additionalAttributes), + this.pid) + ); + } + + for (RegisteredProfileAttribute profileAttribute : this.attributes.items()) { + if (profileAttribute.violatesMandatoryProperty(pidRecord)) { + throw new RecordValidationException( + pidRecord, + String.format("Attribute %s missing, but is mandatory in profile %s", + profileAttribute.pid(), + this.pid) + ); + } + if (profileAttribute.violatesRepeatableProperty(pidRecord)) { + throw new RecordValidationException( + pidRecord, + String.format("Attribute %s is not repeatable in profile %s, but has multiple values", + profileAttribute.pid(), + this.pid) + ); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/kit/datamanager/pit/typeregistry/RegisteredProfileAttribute.java b/src/main/java/edu/kit/datamanager/pit/typeregistry/RegisteredProfileAttribute.java new file mode 100644 index 00000000..5a85d940 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/pit/typeregistry/RegisteredProfileAttribute.java @@ -0,0 +1,21 @@ +package edu.kit.datamanager.pit.typeregistry; + +import edu.kit.datamanager.pit.common.RecordValidationException; +import edu.kit.datamanager.pit.domain.PIDRecord; + +public record RegisteredProfileAttribute( + String pid, + boolean mandatory, + boolean repeatable +) { + public boolean violatesMandatoryProperty(PIDRecord pidRecord) { + boolean contains = pidRecord.getPropertyIdentifiers().contains(this.pid) + && pidRecord.getPropertyValues(this.pid).length > 0; + return this.mandatory && !contains; + } + + public boolean violatesRepeatableProperty(PIDRecord pidRecord) { + boolean repeats = pidRecord.getPropertyValues(this.pid).length > 1; + return !this.repeatable && repeats; + } +} diff --git a/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/TypeApi.java b/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/TypeApi.java new file mode 100644 index 00000000..db3a06d8 --- /dev/null +++ b/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/TypeApi.java @@ -0,0 +1,166 @@ +package edu.kit.datamanager.pit.typeregistry.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import edu.kit.datamanager.pit.Application; +import edu.kit.datamanager.pit.common.ExternalServiceException; +import edu.kit.datamanager.pit.common.TypeNotFoundException; +import edu.kit.datamanager.pit.configuration.ApplicationProperties; +import edu.kit.datamanager.pit.domain.ImmutableList; +import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; +import edu.kit.datamanager.pit.typeregistry.RegisteredProfile; +import edu.kit.datamanager.pit.typeregistry.RegisteredProfileAttribute; +import org.everit.json.schema.Schema; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.client.RestClient; + +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class TypeApi implements ITypeRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(TypeApi.class); + // TODO put into config class to read it from application.properties + private static final String BASE_URL = "https://typeapi.lab.pidconsortium.net"; + public static final String SERVICE_NAME = "Type-Api"; + + protected final RestClient http; + protected final AsyncLoadingCache schemaCache; + protected final AsyncLoadingCache profileCache; + + public TypeApi(ApplicationProperties properties) throws URISyntaxException { + String baseUri = new URI(BASE_URL).resolve("v1/types").toString(); + this.http = RestClient.builder().baseUrl(baseUri).build(); + + // TODO better name caching properties (and consider extending them) + int maximumSize = properties.getMaximumSize(); + long expireAfterWrite = properties.getExpireAfterWrite(); + + this.schemaCache = Caffeine.newBuilder() + .maximumSize(maximumSize) + .executor(Application.EXECUTOR) + .refreshAfterWrite(Duration.ofMinutes(expireAfterWrite / 2)) + .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) + .removalListener((key, value, cause) -> + LOG.trace("Removing schema of {} from schema cache. Cause: {}", key, cause) + ) + .buildAsync(maybeSchemaPid -> { + LOG.trace("Loading schema for identifier {} to cache.", maybeSchemaPid); + return this.querySchema(maybeSchemaPid); + }); + + this.profileCache = Caffeine.newBuilder() + .maximumSize(maximumSize) + .executor(Application.EXECUTOR) + .refreshAfterWrite(Duration.ofMinutes(expireAfterWrite / 2)) + .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) + .removalListener((key, value, cause) -> + LOG.trace("Removing profile {} from profile cache. Cause: {}", key, cause) + ) + .buildAsync(maybeProfilePid -> { + LOG.trace("Loading profile {} to cache.", maybeProfilePid); + return this.queryProfile(maybeProfilePid); + }); + } + + protected Schema querySchema(String maybeSchemaPid) throws TypeNotFoundException, ExternalServiceException { + return http.get() + .uri(uriBuilder -> uriBuilder + .pathSegment("schema") + .path(maybeSchemaPid) + .build()) + .exchange((clientRequest, clientResponse) -> { + if (clientResponse.getStatusCode().is2xxSuccessful()) { + InputStream inputStream = clientResponse.getBody(); + Schema schema; + try { + JSONObject rawSchema = new JSONObject(new JSONTokener(inputStream)); + schema = SchemaLoader.load(rawSchema); + } catch (JSONException e) { + throw new ExternalServiceException(SERVICE_NAME, "Response (" + maybeSchemaPid + ") is not a valid schema."); + } finally { + inputStream.close(); + } + return schema; + } else { + throw new TypeNotFoundException(maybeSchemaPid); + } + }); + } + + protected RegisteredProfile queryProfile(String maybeProfilePid) throws TypeNotFoundException, ExternalServiceException { + return http.get() + .uri(uriBuilder -> uriBuilder + .path(maybeProfilePid) + .build()) + .exchange((clientRequest, clientResponse) -> { + if (clientResponse.getStatusCode().is2xxSuccessful()) { + InputStream inputStream = clientResponse.getBody(); + String body = new String(inputStream.readAllBytes()); + inputStream.close(); + return extractProfileInformation(maybeProfilePid, Application.jsonObjectMapper().readTree(body)); + } else { + throw new TypeNotFoundException(maybeProfilePid); + } + }); + } + + protected RegisteredProfile extractProfileInformation(String profilePid, JsonNode typeApiResponse) + throws TypeNotFoundException, ExternalServiceException { + + List attributes = new ArrayList<>(); + typeApiResponse.path("content").path("properties").forEach(item -> { + + String attributePid = Optional.ofNullable(item.path("pid").asText(null)) + .or(() -> Optional.ofNullable(item.path("identifier").asText(null))) + .or(() -> Optional.ofNullable(item.path("id").asText())) + .orElse(""); + + JsonNode representations = item.path("representationsAndSemantics").path(0); + + JsonNode obligationNode = representations.path("obligation"); + boolean attributeMandatory = obligationNode.isBoolean() ? obligationNode.asBoolean() + : List.of("mandatory", "yes", "true").contains(obligationNode.asText().trim().toLowerCase()); + + JsonNode repeatableNode = representations.path("repeatable"); + boolean attributeRepeatable = repeatableNode.isBoolean() ? repeatableNode.asBoolean() + : List.of("yes", "true", "repeatable").contains(repeatableNode.asText().trim().toLowerCase()); + + RegisteredProfileAttribute attribute = new RegisteredProfileAttribute( + attributePid, + attributeMandatory, + attributeRepeatable); + + if (obligationNode.isNull() || repeatableNode.isNull() || attributePid.trim().isEmpty()) { + throw new ExternalServiceException(SERVICE_NAME, "Malformed attribute in profile (%s): " + attribute); + } + attributes.add(attribute); + + }); + + return new RegisteredProfile(profilePid, new ImmutableList<>(attributes)); + } + + @Override + public CompletableFuture queryAttributeSchemaOf(String attributePid) { + return this.schemaCache.get(attributePid); + } + + @Override + public CompletableFuture queryAsProfile(String profilePid) { + return this.profileCache.get(profilePid); + } +} diff --git a/src/main/java/edu/kit/datamanager/pit/util/TypeValidationUtils.java b/src/main/java/edu/kit/datamanager/pit/util/TypeValidationUtils.java index 5b062727..170fd3c6 100644 --- a/src/main/java/edu/kit/datamanager/pit/util/TypeValidationUtils.java +++ b/src/main/java/edu/kit/datamanager/pit/util/TypeValidationUtils.java @@ -16,6 +16,7 @@ * * @author Thomas Jejkal */ +@Deprecated public class TypeValidationUtils { private TypeValidationUtils() {} @@ -29,6 +30,7 @@ private TypeValidationUtils() {} * shows all missing attributes in its error * message. */ + @Deprecated public static void checkMandatoryAttributes(PIDRecord pidRecord, TypeDefinition profile) throws RecordValidationException { Collection missing = pidRecord.getMissingMandatoryTypesOf(profile); diff --git a/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/TypeApiTest.java b/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/TypeApiTest.java new file mode 100644 index 00000000..f67d506f --- /dev/null +++ b/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/TypeApiTest.java @@ -0,0 +1,20 @@ +package edu.kit.datamanager.pit.typeregistry.impl; + +import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestClient; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.junit.jupiter.api.Assertions.*; + +class TypeApiTest { + @Test + void testUriCreation() throws URISyntaxException { + String url = new URI("https://example.com").resolve("v1/types").toString(); + assertEquals("https://example.com/v1/types", url); + RestClient.create(url).get().uri(uriBuilder -> { + return uriBuilder.build(); + }); + } +} \ No newline at end of file