diff --git a/src/main/java/edu/kit/datamanager/pit/Application.java b/src/main/java/edu/kit/datamanager/pit/Application.java index 0de3b205..219118ac 100644 --- a/src/main/java/edu/kit/datamanager/pit/Application.java +++ b/src/main/java/edu/kit/datamanager/pit/Application.java @@ -1,265 +1,265 @@ -/* - * Copyright 2018 Karlsruhe Institute of Technology. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package edu.kit.datamanager.pit; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.github.benmanes.caffeine.cache.AsyncLoadingCache; - -import com.github.benmanes.caffeine.cache.Caffeine; -import edu.kit.datamanager.pit.cli.CliTaskBootstrap; -import edu.kit.datamanager.pit.cli.CliTaskWriteFile; -import edu.kit.datamanager.pit.cli.ICliTask; -import edu.kit.datamanager.pit.cli.PidSource; -import edu.kit.datamanager.pit.common.InvalidConfigException; -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.pidsystem.IIdentifierSystem; -import edu.kit.datamanager.pit.pitservice.ITypingService; -import edu.kit.datamanager.pit.pitservice.impl.TypingService; -import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; -import edu.kit.datamanager.pit.typeregistry.impl.TypeRegistry; -import edu.kit.datamanager.pit.web.converter.SimplePidRecordConverter; -import edu.kit.datamanager.security.filter.KeycloakJwtProperties; - -import java.io.IOException; -import java.time.Duration; -import java.util.Objects; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.cache.CacheConfig; -import org.apache.http.impl.client.cache.CachingHttpClientBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.beans.factory.InjectionPoint; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Scope; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.scheduling.annotation.EnableScheduling; - -/** - * - * @author jejkal - */ -@SpringBootApplication -@EnableScheduling -@EntityScan({ "edu.kit.datamanager" }) -// Required for "DAO" objects to work, needed for messaging service and database -// mappings -@EnableJpaRepositories("edu.kit.datamanager") -// Detects services and components in datamanager dependencies (service-base and -// repo-core) -@ComponentScan({ "edu.kit.datamanager" }) -public class Application { - - private static final Logger LOG = LoggerFactory.getLogger(Application.class); - - protected static final String CMD_BOOTSTRAP = "bootstrap"; - protected static final String CMD_WRITE_FILE = "write-file"; - - protected static final String SOURCE_FROM_PREFIX = "all-pids-from-prefix"; - protected static final String SOURCE_KNOWN_PIDS = "known-pids"; - - protected static final String ERROR_COMMUNICATION = "Communication error: {}"; - protected static final String ERROR_CONFIGURATION = "Configuration error: {}"; - - protected static final Executor EXECUTOR = Executors.newWorkStealingPool(); - - - @Bean - @Scope("prototype") - public Logger logger(InjectionPoint injectionPoint) { - Class targetClass = injectionPoint.getMember().getDeclaringClass(); - return LoggerFactory.getLogger(targetClass.getCanonicalName()); - } - - @Bean - public ITypeRegistry typeRegistry() { - return new TypeRegistry(); - } - - @Bean - public ITypingService typingService(IIdentifierSystem identifierSystem, ApplicationProperties props) { - return new TypingService(identifierSystem, typeRegistry(), typeLoader(props)); - } - - @Bean(name = "OBJECT_MAPPER_BEAN") - public static ObjectMapper jsonObjectMapper() { - return Jackson2ObjectMapperBuilder.json() - .serializationInclusion(JsonInclude.Include.NON_EMPTY) // Don’t include null values - .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // ISODate - .modules(new JavaTimeModule()) - .build(); - } - - @Bean - public HttpClient httpClient() { - return CachingHttpClientBuilder - .create() - .setCacheConfig(cacheConfig()) - .build(); - } - - @Bean - public CacheConfig cacheConfig() { - return CacheConfig - .custom() - .setMaxObjectSize(500000) // 500KB - .setMaxCacheEntries(2000) - // Set this to false and a response with queryString - // will be cached when it is explicitly cacheable - // .setNeverCacheHTTP10ResponsesWithQueryString(false) - .build(); - } - - /** - * This loader is a cache, which will retrieve `TypeDefinition`s, if required. - * - * Therefore, it can be used instead of the ITypeRegistry implementations. - * Retrieve it using Autowire or from the application context. - * - * @param props the applications properties set by the administration at the - * start of this application. - * @return the cache - */ - @Bean - public AsyncLoadingCache typeLoader(ApplicationProperties props) { - int maximumSize = props.getMaximumSize(); - long expireAfterWrite = props.getExpireAfterWrite(); - return Caffeine.newBuilder() - .maximumSize(maximumSize) - .executor(EXECUTOR) - .refreshAfterWrite(Duration.ofMinutes(expireAfterWrite / 2)) - .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) - .removalListener((key, value, cause) -> - LOG.trace("Removing type definition located at {} from schema cache. Cause: {}", key, cause) - ) - .buildAsync(pid -> { - LOG.trace("Loading type definition for identifier {} to cache.", pid); - return typeRegistry().queryTypeDefinition(pid); - }); - } - - @ConfigurationProperties("pit") - public ApplicationProperties applicationProperties() { - return new ApplicationProperties(); - } - - @Bean - // Reads keycloak related settings from properties.application. - public KeycloakJwtProperties properties() { - return new KeycloakJwtProperties(); - } - - @Bean - public HttpMessageConverter simplePidRecordConverter() { - return new SimplePidRecordConverter(); - } - - public static void main(String[] args) { - ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); - System.out.println("Spring is running!"); - - final boolean cliArgsAmountValid = args != null && args.length != 0 && args.length >= 2; - - if (cliArgsAmountValid) { - ICliTask task = null; - Stream pidSource = null; - - if (Objects.equals(args[1], SOURCE_FROM_PREFIX)) { - try { - pidSource = PidSource.fromPrefix(context); - } catch (IOException e) { - e.printStackTrace(); - LOG.error(ERROR_COMMUNICATION, e.getMessage()); - exitApp(context, 1); - } - } else if (Objects.equals(args[1], SOURCE_KNOWN_PIDS)) { - pidSource = PidSource.fromKnown(context); - } - - if (Objects.equals(args[0], CMD_BOOTSTRAP)) { - task = new CliTaskBootstrap(context, pidSource); - } else if (Objects.equals(args[0], CMD_WRITE_FILE)) { - task = new CliTaskWriteFile(pidSource); - } - - try { - if (task != null && pidSource != null) { - // ---process task--- - if (task.process()) { - exitApp(context, 0); - } - } else { - printUsage(args); - exitApp(context, 1); - } - } catch (InvalidConfigException e) { - e.printStackTrace(); - LOG.error(ERROR_CONFIGURATION, e.getMessage()); - exitApp(context, 1); - } catch (IOException e) { - e.printStackTrace(); - LOG.error(ERROR_COMMUNICATION, e.getMessage()); - exitApp(context, 1); - } - } - } - - private static void printUsage(String[] args) { - String firstArg = args[0].replaceAll("[\r\n]",""); - String secondArg = args[1].replaceAll("[\r\n]",""); - LOG.error("Got commands: {} and {}", firstArg, secondArg); - LOG.error("CLI usage incorrect. Usage:"); - LOG.error("java -jar TypedPIDMaker.jar [ACTION] [SOURCE]"); - LOG.error("java -jar TypedPIDMaker.jar bootstrap all-pids-from-prefix"); - LOG.error("java -jar TypedPIDMaker.jar bootstrap known-pids"); - LOG.error("java -jar TypedPIDMaker.jar write-file all-pids-from-prefix"); - LOG.error("java -jar TypedPIDMaker.jar write-file known-pids"); - } - - private static void exitApp(ConfigurableApplicationContext context, int errCode) { - context.close(); - try { - Thread.sleep(2 * 1000L); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - if (errCode != 0) { - LOG.error("Exited with error."); - } else { - LOG.info("Success"); - } - System.exit(errCode); - } - -} +/* + * Copyright 2018 Karlsruhe Institute of Technology. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package edu.kit.datamanager.pit; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import edu.kit.datamanager.pit.cli.CliTaskBootstrap; +import edu.kit.datamanager.pit.cli.CliTaskWriteFile; +import edu.kit.datamanager.pit.cli.ICliTask; +import edu.kit.datamanager.pit.cli.PidSource; +import edu.kit.datamanager.pit.common.InvalidConfigException; +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.pidsystem.IIdentifierSystem; +import edu.kit.datamanager.pit.pitservice.ITypingService; +import edu.kit.datamanager.pit.pitservice.impl.TypingService; +import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; +import edu.kit.datamanager.pit.typeregistry.impl.DtrTest; +import edu.kit.datamanager.pit.web.converter.SimplePidRecordConverter; +import edu.kit.datamanager.security.filter.KeycloakJwtProperties; + +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.CachingHttpClientBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Scope; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * + * @author jejkal + */ +@SpringBootApplication +@EnableScheduling +@EntityScan({ "edu.kit.datamanager" }) +// Required for "DAO" objects to work, needed for messaging service and database +// mappings +@EnableJpaRepositories("edu.kit.datamanager") +// Detects services and components in datamanager dependencies (service-base and +// repo-core) +@ComponentScan({ "edu.kit.datamanager" }) +public class Application { + + private static final Logger LOG = LoggerFactory.getLogger(Application.class); + + protected static final String CMD_BOOTSTRAP = "bootstrap"; + protected static final String CMD_WRITE_FILE = "write-file"; + + protected static final String SOURCE_FROM_PREFIX = "all-pids-from-prefix"; + protected static final String SOURCE_KNOWN_PIDS = "known-pids"; + + protected static final String ERROR_COMMUNICATION = "Communication error: {}"; + protected static final String ERROR_CONFIGURATION = "Configuration error: {}"; + + protected static final Executor EXECUTOR = Executors.newWorkStealingPool(); + + + @Bean + @Scope("prototype") + public Logger logger(InjectionPoint injectionPoint) { + Class targetClass = injectionPoint.getMember().getDeclaringClass(); + return LoggerFactory.getLogger(targetClass.getCanonicalName()); + } + + @Bean + public ITypeRegistry typeRegistry() { + return new DtrTest(); + } + + @Bean + public ITypingService typingService(IIdentifierSystem identifierSystem, ApplicationProperties props) { + return new TypingService(identifierSystem, typeRegistry(), typeLoader(props)); + } + + @Bean(name = "OBJECT_MAPPER_BEAN") + public static ObjectMapper jsonObjectMapper() { + return Jackson2ObjectMapperBuilder.json() + .serializationInclusion(JsonInclude.Include.NON_EMPTY) // Don’t include null values + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // ISODate + .modules(new JavaTimeModule()) + .build(); + } + + @Bean + public HttpClient httpClient() { + return CachingHttpClientBuilder + .create() + .setCacheConfig(cacheConfig()) + .build(); + } + + @Bean + public CacheConfig cacheConfig() { + return CacheConfig + .custom() + .setMaxObjectSize(500000) // 500KB + .setMaxCacheEntries(2000) + // Set this to false and a response with queryString + // will be cached when it is explicitly cacheable + // .setNeverCacheHTTP10ResponsesWithQueryString(false) + .build(); + } + + /** + * This loader is a cache, which will retrieve `TypeDefinition`s, if required. + * + * Therefore, it can be used instead of the ITypeRegistry implementations. + * Retrieve it using Autowire or from the application context. + * + * @param props the applications properties set by the administration at the + * start of this application. + * @return the cache + */ + @Bean + public AsyncLoadingCache typeLoader(ApplicationProperties props) { + int maximumSize = props.getMaximumSize(); + long expireAfterWrite = props.getExpireAfterWrite(); + return Caffeine.newBuilder() + .maximumSize(maximumSize) + .executor(EXECUTOR) + .refreshAfterWrite(Duration.ofMinutes(expireAfterWrite / 2)) + .expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES) + .removalListener((key, value, cause) -> + LOG.trace("Removing type definition located at {} from schema cache. Cause: {}", key, cause) + ) + .buildAsync(pid -> { + LOG.trace("Loading type definition for identifier {} to cache.", pid); + return typeRegistry().queryTypeDefinition(pid); + }); + } + + @ConfigurationProperties("pit") + public ApplicationProperties applicationProperties() { + return new ApplicationProperties(); + } + + @Bean + // Reads keycloak related settings from properties.application. + public KeycloakJwtProperties properties() { + return new KeycloakJwtProperties(); + } + + @Bean + public HttpMessageConverter simplePidRecordConverter() { + return new SimplePidRecordConverter(); + } + + public static void main(String[] args) { + ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); + System.out.println("Spring is running!"); + + final boolean cliArgsAmountValid = args != null && args.length != 0 && args.length >= 2; + + if (cliArgsAmountValid) { + ICliTask task = null; + Stream pidSource = null; + + if (Objects.equals(args[1], SOURCE_FROM_PREFIX)) { + try { + pidSource = PidSource.fromPrefix(context); + } catch (IOException e) { + e.printStackTrace(); + LOG.error(ERROR_COMMUNICATION, e.getMessage()); + exitApp(context, 1); + } + } else if (Objects.equals(args[1], SOURCE_KNOWN_PIDS)) { + pidSource = PidSource.fromKnown(context); + } + + if (Objects.equals(args[0], CMD_BOOTSTRAP)) { + task = new CliTaskBootstrap(context, pidSource); + } else if (Objects.equals(args[0], CMD_WRITE_FILE)) { + task = new CliTaskWriteFile(pidSource); + } + + try { + if (task != null && pidSource != null) { + // ---process task--- + if (task.process()) { + exitApp(context, 0); + } + } else { + printUsage(args); + exitApp(context, 1); + } + } catch (InvalidConfigException e) { + e.printStackTrace(); + LOG.error(ERROR_CONFIGURATION, e.getMessage()); + exitApp(context, 1); + } catch (IOException e) { + e.printStackTrace(); + LOG.error(ERROR_COMMUNICATION, e.getMessage()); + exitApp(context, 1); + } + } + } + + private static void printUsage(String[] args) { + String firstArg = args[0].replaceAll("[\r\n]",""); + String secondArg = args[1].replaceAll("[\r\n]",""); + LOG.error("Got commands: {} and {}", firstArg, secondArg); + LOG.error("CLI usage incorrect. Usage:"); + LOG.error("java -jar TypedPIDMaker.jar [ACTION] [SOURCE]"); + LOG.error("java -jar TypedPIDMaker.jar bootstrap all-pids-from-prefix"); + LOG.error("java -jar TypedPIDMaker.jar bootstrap known-pids"); + LOG.error("java -jar TypedPIDMaker.jar write-file all-pids-from-prefix"); + LOG.error("java -jar TypedPIDMaker.jar write-file known-pids"); + } + + private static void exitApp(ConfigurableApplicationContext context, int errCode) { + context.close(); + try { + Thread.sleep(2 * 1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (errCode != 0) { + LOG.error("Exited with error."); + } else { + LOG.info("Success"); + } + System.exit(errCode); + } + +} diff --git a/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/TypeRegistry.java b/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/DtrTest.java similarity index 95% rename from src/main/java/edu/kit/datamanager/pit/typeregistry/impl/TypeRegistry.java rename to src/main/java/edu/kit/datamanager/pit/typeregistry/impl/DtrTest.java index 8e552930..7fb6b999 100644 --- a/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/TypeRegistry.java +++ b/src/main/java/edu/kit/datamanager/pit/typeregistry/impl/DtrTest.java @@ -1,169 +1,167 @@ -package edu.kit.datamanager.pit.typeregistry.impl; - -import java.io.IOException; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.benmanes.caffeine.cache.AsyncLoadingCache; -import edu.kit.datamanager.pit.configuration.ApplicationProperties; -import edu.kit.datamanager.pit.domain.ProvenanceInformation; -import edu.kit.datamanager.pit.domain.TypeDefinition; -import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; -import java.net.URISyntaxException; -import java.time.Instant; -import java.time.format.DateTimeParseException; -import java.util.concurrent.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.apache.commons.lang3.stream.Streams; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * Accessor for a specific instance of a TypeRegistry. The TypeRegistry is - * uniquely identified by a baseUrl and an identifierPrefix which all types of - * this particular registry are using. The prefix also allows to determine, - * whether a given PID might be a type or property registered at this - * TypeRegistry. - */ -public class TypeRegistry implements ITypeRegistry { - - private static final Logger LOG = LoggerFactory.getLogger(TypeRegistry.class); - protected static final Executor EXECUTOR = Executors.newWorkStealingPool(20); - - @Autowired - public AsyncLoadingCache typeCache; - @Autowired - private ApplicationProperties applicationProperties; - - protected RestTemplate restTemplate = new RestTemplate(); - - @Override - public TypeDefinition queryTypeDefinition(String typeIdentifier) throws IOException, URISyntaxException { - LOG.trace("Performing queryTypeDefinition({}).", typeIdentifier); - String[] segments = typeIdentifier.split("/"); - UriComponentsBuilder uriBuilder = UriComponentsBuilder - .fromUri( - applicationProperties - .getHandleBaseUri() - .toURI()) - .pathSegment(segments); - LOG.trace("Querying for type definition at URI {}.", uriBuilder); - ResponseEntity response = restTemplate.exchange(uriBuilder.build().toUri(), HttpMethod.GET, - HttpEntity.EMPTY, String.class); - ObjectMapper mapper = new ObjectMapper(); - JsonNode rootNode = mapper.readTree(response.getBody()); - LOG.trace("Constructing type definition from response."); - return constructTypeDefinition(rootNode); - } - - /** - * Helper method to construct a type definition from a JSON response - * received from the TypeRegistry. - * - * @param registryRepresentation The type definition. - * @return The TypeDefinition as object. - */ - private TypeDefinition constructTypeDefinition(JsonNode registryRepresentation) - throws JsonProcessingException, IOException, URISyntaxException { - // TODO We are doing things too complicated here. Deserialization should be - // easy. - // But before we change the domain model to do so, we need a lot of tests to - // make sure things work as before after the changes. - LOG.trace("Performing constructTypeDefinition()."); - final String identifier = registryRepresentation.path("identifier").asText(null); - if (identifier == null) { - LOG.error("No 'identifier' property found in entry: {}", registryRepresentation); - throw new IOException("No 'identifier' attribute found in type definition."); - } - - LOG.trace("Checking for 'properties' attribute."); - Map properties = new ConcurrentHashMap<>(); - List> propertiesHandling = Streams.stream(StreamSupport.stream( - registryRepresentation.path("properties").spliterator(), false)) - .filter(property -> property.hasNonNull("name")) - .filter(property -> property.hasNonNull("identifier")) - .map(property -> { - final String name = property.path("name").asText(); - final String pid = property.path("identifier").asText(); - return typeCache.get(pid).thenAcceptAsync( - typeDefinition -> { - final JsonNode semantics = property.path("representationsAndSemantics").path(0); - final String expression = semantics.path("expression").asText(null); - typeDefinition.setExpression(expression); - final String value = semantics.path("value").asText(null); - typeDefinition.setValue(value); - final String obligation = semantics.path("obligation").asText("Mandatory"); - typeDefinition.setOptional("Optional".equalsIgnoreCase(obligation)); - final String repeatable = semantics.path("repeatable").asText("No"); - typeDefinition.setRepeatable(!"No".equalsIgnoreCase(repeatable)); - properties.put(name, typeDefinition); - }, EXECUTOR); - }) - .collect(Collectors.toList()); - - TypeDefinition result = new TypeDefinition(); - result.setIdentifier(identifier); - final String description = registryRepresentation.path("description").asText(null); - result.setDescription(description); - final String name = registryRepresentation.path("name").asText(null); - result.setName(name); - final String validationSchema = registryRepresentation.path("validationSchema").asText(null); - result.setSchema(validationSchema); - - if (registryRepresentation.has("provenance")) { - ProvenanceInformation prov = new ProvenanceInformation(); - JsonNode provNode = registryRepresentation.get("provenance"); - if (provNode.has("creationDate")) { - String creationDate = provNode.get("creationDate").asText(); - try { - prov.setCreationDate(Date.from(Instant.parse(creationDate))); - } catch (DateTimeParseException ex) { - LOG.error("Failed to parse creationDate from value " + creationDate + ".", ex); - } - } - if (provNode.has("lastModificationDate")) { - String lastModificationDate = provNode.get("lastModificationDate").asText(); - try { - prov.setLastModificationDate(Date.from(Instant.parse(lastModificationDate))); - } catch (DateTimeParseException ex) { - LOG.error("Failed to parse lastModificationDate from value " + lastModificationDate + ".", ex); - } - } - for (JsonNode entryKV : provNode.get("contributors")) { - String identified = null; - String contributorName = null; - String details = null; - - if (registryRepresentation.has("identifiedBy")) { - identified = entryKV.get("identifiedBy").asText(); - } - if (registryRepresentation.has("name")) { - contributorName = entryKV.get("name").asText(); - } - if (registryRepresentation.has("details")) { - details = entryKV.get("details").asText(); - } - prov.addContributor(identified, contributorName, details); - } - result.setProvenance(prov); - } - - LOG.trace("Finalizing and returning type definition."); - CompletableFuture.allOf(propertiesHandling.toArray(new CompletableFuture[0])).join(); - properties.keySet().forEach(pd -> result.addSubType(properties.get(pd))); - this.typeCache.put(identifier, CompletableFuture.completedFuture(result)); - return result; - } -} +package edu.kit.datamanager.pit.typeregistry.impl; + +import java.io.IOException; +import java.util.Date; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import edu.kit.datamanager.pit.configuration.ApplicationProperties; +import edu.kit.datamanager.pit.domain.ProvenanceInformation; +import edu.kit.datamanager.pit.domain.TypeDefinition; +import edu.kit.datamanager.pit.typeregistry.ITypeRegistry; +import java.net.URISyntaxException; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.concurrent.*; +import java.util.stream.StreamSupport; + +import org.apache.commons.lang3.stream.Streams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Accessor for a specific instance of a TypeRegistry. The TypeRegistry is + * uniquely identified by a baseUrl and an identifierPrefix which all types of + * this particular registry are using. The prefix also allows to determine, + * whether a given PID might be a type or property registered at this + * TypeRegistry. + */ +public class DtrTest implements ITypeRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(DtrTest.class); + protected static final Executor EXECUTOR = Executors.newWorkStealingPool(20); + + @Autowired + public AsyncLoadingCache typeCache; + @Autowired + private ApplicationProperties applicationProperties; + + protected RestTemplate restTemplate = new RestTemplate(); + + @Override + public TypeDefinition queryTypeDefinition(String typeIdentifier) throws IOException, URISyntaxException { + LOG.trace("Performing queryTypeDefinition({}).", typeIdentifier); + String[] segments = typeIdentifier.split("/"); + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUri( + applicationProperties + .getHandleBaseUri() + .toURI()) + .pathSegment(segments); + LOG.trace("Querying for type definition at URI {}.", uriBuilder); + ResponseEntity response = restTemplate.exchange(uriBuilder.build().toUri(), HttpMethod.GET, + HttpEntity.EMPTY, String.class); + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(response.getBody()); + LOG.trace("Constructing type definition from response."); + return constructTypeDefinition(rootNode); + } + + /** + * Helper method to construct a type definition from a JSON response + * received from the TypeRegistry. + * + * @param registryRepresentation The type definition. + * @return The TypeDefinition as object. + */ + private TypeDefinition constructTypeDefinition(JsonNode registryRepresentation) + throws JsonProcessingException, IOException, URISyntaxException { + // TODO We are doing things too complicated here. Deserialization should be + // easy. + // But before we change the domain model to do so, we need a lot of tests to + // make sure things work as before after the changes. + LOG.trace("Performing constructTypeDefinition()."); + final String identifier = registryRepresentation.path("identifier").asText(null); + if (identifier == null) { + LOG.error("No 'identifier' property found in entry: {}", registryRepresentation); + throw new IOException("No 'identifier' attribute found in type definition."); + } + + LOG.trace("Checking for 'properties' attribute."); + Map properties = new ConcurrentHashMap<>(); + List> propertiesHandling = Streams.stream(StreamSupport.stream( + registryRepresentation.path("properties").spliterator(), false)) + .filter(property -> property.hasNonNull("name")) + .filter(property -> property.hasNonNull("identifier")) + .map(property -> { + final String name = property.path("name").asText(); + final String pid = property.path("identifier").asText(); + return typeCache.get(pid).thenAcceptAsync( + typeDefinition -> { + final JsonNode semantics = property.path("representationsAndSemantics").path(0); + final String expression = semantics.path("expression").asText(null); + typeDefinition.setExpression(expression); + final String value = semantics.path("value").asText(null); + typeDefinition.setValue(value); + final String obligation = semantics.path("obligation").asText("Mandatory"); + typeDefinition.setOptional("Optional".equalsIgnoreCase(obligation)); + final String repeatable = semantics.path("repeatable").asText("No"); + typeDefinition.setRepeatable(!"No".equalsIgnoreCase(repeatable)); + properties.put(name, typeDefinition); + }, EXECUTOR); + }) + .collect(Collectors.toList()); + + TypeDefinition result = new TypeDefinition(); + result.setIdentifier(identifier); + final String description = registryRepresentation.path("description").asText(null); + result.setDescription(description); + final String name = registryRepresentation.path("name").asText(null); + result.setName(name); + final String validationSchema = registryRepresentation.path("validationSchema").asText(null); + result.setSchema(validationSchema); + + if (registryRepresentation.has("provenance")) { + ProvenanceInformation prov = new ProvenanceInformation(); + JsonNode provNode = registryRepresentation.get("provenance"); + if (provNode.has("creationDate")) { + String creationDate = provNode.get("creationDate").asText(); + try { + prov.setCreationDate(Date.from(Instant.parse(creationDate))); + } catch (DateTimeParseException ex) { + LOG.error("Failed to parse creationDate from value " + creationDate + ".", ex); + } + } + if (provNode.has("lastModificationDate")) { + String lastModificationDate = provNode.get("lastModificationDate").asText(); + try { + prov.setLastModificationDate(Date.from(Instant.parse(lastModificationDate))); + } catch (DateTimeParseException ex) { + LOG.error("Failed to parse lastModificationDate from value " + lastModificationDate + ".", ex); + } + } + for (JsonNode entryKV : provNode.get("contributors")) { + String identified = null; + String contributorName = null; + String details = null; + + if (registryRepresentation.has("identifiedBy")) { + identified = entryKV.get("identifiedBy").asText(); + } + if (registryRepresentation.has("name")) { + contributorName = entryKV.get("name").asText(); + } + if (registryRepresentation.has("details")) { + details = entryKV.get("details").asText(); + } + prov.addContributor(identified, contributorName, details); + } + result.setProvenance(prov); + } + + LOG.trace("Finalizing and returning type definition."); + CompletableFuture.allOf(propertiesHandling.toArray(new CompletableFuture[0])).join(); + properties.keySet().forEach(pd -> result.addSubType(properties.get(pd))); + this.typeCache.put(identifier, CompletableFuture.completedFuture(result)); + return result; + } +} diff --git a/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/TypeRegistryTest.java b/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/DtrTestTest.java similarity index 96% rename from src/test/java/edu/kit/datamanager/pit/typeregistry/impl/TypeRegistryTest.java rename to src/test/java/edu/kit/datamanager/pit/typeregistry/impl/DtrTestTest.java index 2bb7db38..b25ba600 100644 --- a/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/TypeRegistryTest.java +++ b/src/test/java/edu/kit/datamanager/pit/typeregistry/impl/DtrTestTest.java @@ -18,10 +18,10 @@ // Set the in-memory implementation @TestPropertySource(locations = "/test/application-test.properties", properties = "pit.pidsystem.implementation = LOCAL") @ActiveProfiles("test") -class TypeRegistryTest { +class DtrTestTest { @Autowired - TypeRegistry typeRegistry; + DtrTest typeRegistry; final String profileIdentifier = "21.T11148/b9b76f887845e32d29f7";