From dd4db88bc6f7c70b96e96cc25ada1b345fce7ded Mon Sep 17 00:00:00 2001 From: Timm Fitschen <t.fitschen@indiscale.com> Date: Thu, 29 Feb 2024 00:07:02 +0100 Subject: [PATCH] WIP: create fdo --- api/pom.xml | 36 +- api/src/main/resources/api.yaml | 372 +++++++++++++++++- application/pom.xml | 65 ++- .../fdo/manager/service/BaseController.java | 32 ++ .../fdo/manager/service/ManagerPool.java | 67 ++++ .../fdo/manager/service/ProcessDatabase.java | 49 +++ .../fdo/manager/service/fdo/FDOApiImpl.java | 80 ++++ .../fdo/manager/service/fdo/PIDApiImpl.java | 44 +++ .../fdo/manager/service/hello/HelloImpl.java | 2 +- .../fdo/manager/service/hello/InfoImpl.java | 27 ++ .../fdo/manager/service/mock/MockManager.java | 48 +++ .../service/mock/MockRepositoryFactory.java | 97 +++++ .../repositories/RepositoriesImpl.java | 101 +++++ .../src/main/resources/application.properties | 6 + .../hello/HelloEmbeddedServerTest.java | 7 +- .../service/hello/HelloMockMvcTest.java | 9 +- .../repositories/RepositoriesTest.java | 42 ++ pom.xml | 2 +- 18 files changed, 1039 insertions(+), 47 deletions(-) create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/BaseController.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/ManagerPool.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/ProcessDatabase.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/fdo/FDOApiImpl.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/fdo/PIDApiImpl.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/hello/InfoImpl.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/mock/MockManager.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/mock/MockRepositoryFactory.java create mode 100644 application/src/main/java/com/indiscale/fdo/manager/service/repositories/RepositoriesImpl.java create mode 100644 application/src/test/java/com/indiscale/fdo/manager/service/repositories/RepositoriesTest.java diff --git a/api/pom.xml b/api/pom.xml index 98a16c3..5456244 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -13,10 +13,10 @@ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.outputEncoding>UTF-8</project.build.outputEncoding> <sourceDir>/src/main/java/</sourceDir> - <apiBasePackage>${groupId}.manager.service.api</apiBasePackage> + <apiBasePackage>${project.groupId}.manager.service.api</apiBasePackage> </properties> - <artifactId>${parent.artifactId}.api</artifactId> + <artifactId>fdo-manager-service.api</artifactId> <packaging>jar</packaging> <name>${project.artifactId}</name> @@ -59,7 +59,7 @@ <configuration> <inputSpec>${basedir}/src/main/resources/api.yaml</inputSpec> <generatorName>spring</generatorName> - <library>spring-boot</library> + <!--<library>spring-boot</library>--> <modelNameSuffix></modelNameSuffix> <generateApis>true</generateApis> @@ -78,42 +78,20 @@ <!-- pass any necessary config options --> <configOptions> - <interfaceOnly>true</interfaceOnly>∂ + <sourceFolder>${sourceDir}</sourceFolder> <useBeanValidation>true</useBeanValidation> <performBeanValidation>true</performBeanValidation> + <interfaceOnly>true</interfaceOnly> + <serializableModel>true</serializableModel> + <implFolder>${sourceDir}</implFolder> <modelPackage>${apiBasePackage}.model</modelPackage> <apiPackage>${apiBasePackage}.operation</apiPackage> - <sourceFolder>${sourceDir}</sourceFolder> - <implFolder>${sourceDir}</implFolder> - <serializableModel>true</serializableModel> - <useJakartaEe>true</useJakartaEe> <useSpringBoot3>true</useSpringBoot3> - </configOptions> </configuration> </execution> </executions> </plugin> - <!-- - <plugin> - <groupId>org.codehaus.mojo</groupId> - <artifactId>properties-maven-plugin</artifactId> - <version>1.0.0</version> - <executions> - <execution> - <phase>initialize</phase> - <goals> - <goal>read-project-properties</goal> - </goals> - <configuration> - <files> - <file>${basedir}/src/main/resources/swagger.properties</file> - </files> - </configuration> - </execution> - </executions> - </plugin> - --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> diff --git a/api/src/main/resources/api.yaml b/api/src/main/resources/api.yaml index 0ab2804..fabb6fe 100644 --- a/api/src/main/resources/api.yaml +++ b/api/src/main/resources/api.yaml @@ -1,4 +1,4 @@ -openapi: 3.1.0 # version of the specification +openapi: "3.1.0" info: version: '0.1' title: 'FDO Manager Service API' @@ -6,6 +6,10 @@ info: servers: - url: http://localhost:8080 + +tags: + - name: Repositories + description: Repositories for storing FDO data and metadata. paths: /hello: get: @@ -18,9 +22,302 @@ paths: application/json: schema: $ref: '#/components/schemas/Hello' - + /info: + get: + summary: Retrieve general information on the service. + operationId: getInfo + responses: + 200: + description: General service information. + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: "#/components/schemas/Info" + links: + $ref: '#/components/schemas/Links' + /repositories: + get: + tags: + - Repositories + summary: "List trusted repositories." + operationId: listRepositories + responses: + 200: + description: "Success." + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Repository' + links: + $ref: '#/components/schemas/Links' + /repositories/{repositoryId}: + get: + tags: + - Repositories + summary: "Get information on a single repository." + operationId: getRepository + parameters: + - name: repositoryId + in: path + description: "Repository ID" + required: true + schema: + $ref: '#/components/schemas/RepositoryID' + responses: + 200: + description: "Success." + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: '#/components/schemas/Repository' + links: + $ref: '#/components/schemas/Links' + 404: + description: "Unknown repository id." + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /fdo: + post: + summary: "Create an FDO." + operationId: createFDO + requestBody: + content: + multipart/form-data: + schema: + type: object + required: ["repositories", "data", "metadata"] + properties: + repositories: + $ref: "#/components/schemas/TargetRepositories" + data: + type: string + format: binary + metadata: + type: string + format: binary + responses: + 201: + description: "The Location header points to the newly created FDO." + /pid/{pid}: + get: + summary: "Resolve a pid." + operationId: "resolvePID" + parameters: + - $ref: '#/components/parameters/PID' + responses: + 200: + description: "A successfully resolved pid." + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: '#/components/schemas/DigitalObject' + links: + $ref: '#/components/schemas/Links' + 404: + description: "Could not resolve pid." + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /fdo/create: + post: + summary: "Initiate the process to create an FDO" + operationId: initiateFdoCreate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFDORequest' + responses: + 201: + description: "Initiated the process to create an FDO. The Location header points to the newly created process resource." + /fdo/create/{processId}: + get: + summary: "Get an overview of the FDO creation process." + operationId: overviewFdoCreate + parameters: + - $ref: '#/components/parameters/ProcessID' + responses: + 200: + description: "Overview of the FDO Creation process." + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: '#/components/schemas/CreateFDOProcess' + links: + $ref: '#/components/schemas/Links' + 404: + $ref: '#/components/responses/404UnknownProcessID' + /fdo/create/{processId}/state: + get: + summary: "Get the current state of the FDO creation process." + operationId: getStateFdoCreate + parameters: + - $ref: '#/components/parameters/ProcessID' + responses: + 200: + $ref: "#/components/responses/200CreateFDOProcessStateResponse" + 404: + $ref: '#/components/responses/404UnknownProcessID' + put: + summary: "Update the current state of the FDO creation process. Use this to cancel or conclude the creation process" + operationId: updateStateFdoCreate + parameters: + - $ref: '#/components/parameters/ProcessID' + requestBody: + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: '#/components/schemas/CreateFDOProcessState' + responses: + 200: + $ref: "#/components/responses/200CreateFDOProcessStateResponse" + 404: + $ref: '#/components/responses/404UnknownProcessID' + /fdo/create/{processId}/metadata: + get: + summary: "Get the current state of the metadata file of the FDO." + operationId: getStatusFdoCreateMetadata + parameters: + - $ref: '#/components/parameters/ProcessID' + responses: + 200: + $ref: "#/components/responses/200UploadStateResponse" + 404: + $ref: '#/components/responses/404UnknownProcessID' + put: + summary: "Upload the metadata file of the FDO." + operationId: uploadFdoCreateMetadata + parameters: + - $ref: '#/components/parameters/ProcessID' + requestBody: + $ref: "#/components/requestBodies/UploadSingleFileRequest" + responses: + 200: + $ref: "#/components/responses/200UploadStateResponse" + 404: + $ref: '#/components/responses/404UnknownProcessID' + /fdo/create/{processId}/data: + get: + summary: "Get the current state of the data file of the FDO." + operationId: getStatusFdoCreateData + parameters: + - $ref: '#/components/parameters/ProcessID' + responses: + 200: + $ref: "#/components/responses/200UploadStateResponse" + 404: + $ref: '#/components/responses/404UnknownProcessID' + put: + summary: "Upload the data file of the FDO." + operationId: uploadFdoCreateData + parameters: + - $ref: '#/components/parameters/ProcessID' + requestBody: + $ref: "#/components/requestBodies/UploadSingleFileRequest" + responses: + 200: + $ref: "#/components/responses/200UploadStateResponse" + 404: + $ref: '#/components/responses/404UnknownProcessID' components: schemas: + UploadState: + type: object + properties: + state: + $ref: '#/components/schemas/CreateFDOProcessState' + CreateFDOProcess: + type: object + required: ["stat2", "processId", "targetRepository"] + properties: + state: + $ref: '#/components/schemas/CreateFDOProcessState' + processId: + type: string + targetRepository: + $ref: '#/components/schemas/TargetRepositories' + CreateFDOProcessState: + type: string + enum: ["processing", "finished", "canceled"] + CreateFDORequest: + type: object + required: ["targetRepository"] + properties: + targetRepository: + $ref: '#/components/schemas/TargetRepositories' + TargetRepositories: + type: object + required: ["fdo"] + properties: + fdo: + type: string + metadata: + type: string + data: + type: string + + Links: + type: object + nullable: true + properties: + self: + type: string + collection: + type: string + nullable: true + Error: + type: object + required: ["detail", "status"] + properties: + detail: + type: string + nullable: true + status: + type: string + nullable: true + RepositoryID: + type: string + nullable: true + examples: + - "gwdg-cordra-1" + - "fdo.indiscale.com" + - "b2share@gwdg" + Repository: + type: object + properties: + id: + $ref: '#/components/schemas/RepositoryID' + links: + $ref: '#/components/schemas/Links' Hello: type: object properties: @@ -28,3 +325,74 @@ components: type: string example: 'Hello marie.curie@sorbonne-universite.fr. This is FDO Manager Service v0.1-SNAPSHOT/API v0.1' default: 'Hello anonymous. This is FDO Manager Service v0.1-SNAPSHOT/API v0.1' + DigitalObject: + type: object + required: ["pid", "isFdo"] + properties: + pid: + type: string + isFdo: + type: boolean + dataPid: + type: string + metadataPid: + type: string + Info: + type: object + properties: + fdoServiceVersion: + type: string + fdoSdkVersion: + type: string + serviceProvider: + type: string + requestBodies: + UploadSingleFileRequest: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + 200CreateFDOProcessStateResponse: + description: "Current state of the FDO Creation process." + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: '#/components/schemas/CreateFDOProcessState' + 200UploadStateResponse: + description: "Current state of the file upload." + content: + application/json: + schema: + type: object + required: ["data"] + properties: + data: + $ref: '#/components/schemas/UploadState' + 404UnknownProcessID: + description: "Unknown process id." + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + parameters: + ProcessID: + name: processId + in: path + description: "Repository ID" + required: true + schema: + $ref: '#/components/schemas/RepositoryID' + PID: + name: pid + in: path + description: "Persistent Identifier" + required: true + schema: + type: string diff --git a/application/pom.xml b/application/pom.xml index a08ddbd..3d4d47c 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -14,39 +14,91 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.outputEncoding>UTF-8</project.build.outputEncoding> + <sourceDir>${project.basedir}/src/main</sourceDir> + <fdo.sdk.version>0.1.0-SNAPSHOT</fdo.sdk.version> </properties> - <artifactId>${parent.artifactId}.application</artifactId> + <artifactId>fdo-manager-service.application</artifactId> <dependencies> + <dependency> + <groupId>com.indiscale.fdo</groupId> + <artifactId>fdo-manager-library</artifactId> + <version>${fdo.sdk.version}</version> + </dependency> + <dependency> <groupId>${project.groupId}</groupId> - <artifactId>${parent.artifactId}.api</artifactId> + <artifactId>${project.parent.artifactId}.api</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-hateoas</artifactId> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> + <dependency> + <groupId>org.springdoc</groupId> + <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> + <version>2.3.0</version> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-context-support</artifactId> + </dependency> </dependencies> + <repositories> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab.indiscale.com/api/v4/projects/229/packages/maven</url> + </repository> +</repositories> + +<distributionManagement> + <repository> + <id>gitlab-maven</id> + <url>https://gitlab.indiscale.com/api/v4/projects/229/packages/maven</url> + </repository> + + <snapshotRepository> + <id>gitlab-maven</id> + <url>https://gitlab.indiscale.com/api/v4/projects/229/packages/maven</url> + </snapshotRepository> +</distributionManagement> + + <build> + <resources> + <resource> + <directory>${sourceDir}/resources</directory> - <build> + <filtering>true</filtering> + </resource> + </resources> <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>3.3.1</version> + <configuration> + <filtering>true</filtering> + </configuration> + </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> - <version>3.2.1</version> <configuration> <mainClass>${project.groupId}.manager.service.Application</mainClass> <skip>false</skip> - <fork>true</fork> </configuration> <executions> <execution> @@ -59,7 +111,6 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> - <version>3.6.0</version> <executions> <execution> <id>copy-api-specification-yaml</id> @@ -71,7 +122,7 @@ <artifactItems> <artifactItem> <groupId>${project.groupId}</groupId> - <artifactId>${parent.artifactId}.api</artifactId> + <artifactId>${project.parent.artifactId}.api</artifactId> <type>jar</type> <overWrite>true</overWrite> <outputDirectory>${project.build.directory}/classes/static/</outputDirectory> diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/BaseController.java b/application/src/main/java/com/indiscale/fdo/manager/service/BaseController.java new file mode 100644 index 0000000..0b4c61c --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/BaseController.java @@ -0,0 +1,32 @@ +package com.indiscale.fdo.manager.service; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.server.ResponseStatusException; + +import com.indiscale.fdo.manager.service.api.model.Error; +import com.indiscale.fdo.manager.service.api.model.Links; + +public class BaseController { + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity<?> handleResponseStatusException(ResponseStatusException ex, WebRequest request) { + Error body = new Error().status(Integer.toString(ex.getStatusCode().value())).detail(ex.getReason()); + return ResponseEntity.status(ex.getStatusCode()).body(body); + } + + public <T> T getOperations(Class<T> controllerClass) { + return methodOn(controllerClass); + } + + public String link(Object object) { + return linkTo(object).withSelfRel().getHref(); + } + + public Links linkSelf(Object object) { + return new Links().self(link(object)); + } +} \ No newline at end of file diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/ManagerPool.java b/application/src/main/java/com/indiscale/fdo/manager/service/ManagerPool.java new file mode 100644 index 0000000..863095d --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/ManagerPool.java @@ -0,0 +1,67 @@ +package com.indiscale.fdo.manager.service; + +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; + +import org.springframework.stereotype.Service; + +import com.indiscale.fdo.api.Data; +import com.indiscale.fdo.api.DigitalObject; +import com.indiscale.fdo.api.FDO; +import com.indiscale.fdo.api.Manager; +import com.indiscale.fdo.api.Metadata; +import com.indiscale.fdo.api.RepositoryConnection; +import com.indiscale.fdo.api.RepositoryRegistry; +import com.indiscale.fdo.manager.service.mock.MockManager; + +@Service +public class ManagerPool { + + public static class PooledManager implements Manager, AutoCloseable { + + final private Manager delegate; + final private Deque<PooledManager> pool; + + public PooledManager(Deque<PooledManager> pool, Manager delegate) { + this.pool = pool; + this.delegate = delegate; + } + + @Override + public void close() { + this.pool.push(this); + } + + @Override + public FDO createFDO(RepositoryConnection repository, Data data, Metadata metadata) { + return delegate.createFDO(repository, data, metadata); + } + + @Override + public RepositoryRegistry getRepositoryRegistry() { + return delegate.getRepositoryRegistry(); + } + + @Override + public DigitalObject resolvePID(String pid) { + return delegate.resolvePID(pid); + } + + } + + private static Deque<PooledManager> pool = new ConcurrentLinkedDeque<>(); + + private Manager createManager(Deque<PooledManager> pool) { + // TODO + return new PooledManager(pool, new MockManager()); + } + + public Manager getManager() { + Manager manager = pool.poll(); + if(manager == null) { + manager = createManager(pool); + } + return manager; + } + +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/ProcessDatabase.java b/application/src/main/java/com/indiscale/fdo/manager/service/ProcessDatabase.java new file mode 100644 index 0000000..cac9265 --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/ProcessDatabase.java @@ -0,0 +1,49 @@ +package com.indiscale.fdo.manager.service; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.springframework.stereotype.Repository; + +import com.indiscale.fdo.manager.service.api.model.CreateFDOProcess; +import com.indiscale.fdo.manager.service.api.model.CreateFDOProcessState; +import com.indiscale.fdo.manager.service.api.model.CreateFDORequest; + +import jakarta.validation.Valid; + +class Process extends CreateFDOProcess { + + private static final long serialVersionUID = -7495201524687733365L; + private @Valid CreateFDORequest createFDORequest; + + public Process(String processId, @Valid CreateFDORequest createFDORequest) { + this.createFDORequest = createFDORequest; + this.setProcessId(processId); + this.setState(CreateFDOProcessState.PROCESSING); + this.setTargetRepository(createFDORequest.getTargetRepository()); + } + +} + +@Repository +public class ProcessDatabase { + + static private Map<String, Process> map = new HashMap<>(); + + public String init(@Valid CreateFDORequest createFDORequest) { + String processId = UUID.randomUUID().toString(); + synchronized (map) { + map.put(processId, new Process(processId, createFDORequest)); + } + return processId; + } + + public CreateFDOProcess get(String processId) { + synchronized (map) { + Process process = map.get(processId); + return process; + } + } + +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/fdo/FDOApiImpl.java b/application/src/main/java/com/indiscale/fdo/manager/service/fdo/FDOApiImpl.java new file mode 100644 index 0000000..1efe256 --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/fdo/FDOApiImpl.java @@ -0,0 +1,80 @@ +package com.indiscale.fdo.manager.service.fdo; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.server.ResponseStatusException; + +import com.indiscale.fdo.api.Data; +import com.indiscale.fdo.api.FDO; +import com.indiscale.fdo.api.InputStreamSource; +import com.indiscale.fdo.api.Manager; +import com.indiscale.fdo.api.Metadata; +import com.indiscale.fdo.api.RepositoryConnection; +import com.indiscale.fdo.api.UnknownRepositoryException; +import com.indiscale.fdo.manager.service.BaseController; +import com.indiscale.fdo.manager.service.ManagerPool; +import com.indiscale.fdo.manager.service.api.model.TargetRepositories; +import com.indiscale.fdo.manager.service.api.operation.FdoApi; + +import jakarta.validation.Valid; + +class MultipartFileWrapper implements InputStreamSource { + + private MultipartFile wrapped; + + public MultipartFileWrapper(MultipartFile file) { + this.wrapped = file; + } + + @Override + public InputStream getInputStream() throws IOException { + return wrapped.getInputStream(); + } +} +class MetadataWrapper extends MultipartFileWrapper implements Metadata { + + public MetadataWrapper(MultipartFile file) { + super(file); + } + +} + +class DataWrapper extends MultipartFileWrapper implements Data { + + public DataWrapper(MultipartFile file) { + super(file); + } + +} + +@RestController +@CrossOrigin(origins = {"${react-dev-server}"}, exposedHeaders = {"Location"}) +public class FDOApiImpl extends BaseController implements FdoApi { + + @Autowired + ManagerPool managerPool; + + + @Override + public ResponseEntity<Void> createFDO(@Valid TargetRepositories repositories, MultipartFile data, + MultipartFile metadata) { + try (Manager manager = managerPool.getManager()) { + RepositoryConnection repository = manager.getRepositoryRegistry().createRepositoryConnection(repositories.getFdo()); + FDO fdo = manager.createFDO(repository, new DataWrapper(data), new MetadataWrapper(metadata)); + URI location = linkTo(getOperations(PIDApiImpl.class).resolvePID(fdo.getPID())).toUri(); + return ResponseEntity.created(location).build(); + } catch (UnknownRepositoryException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not Found. Unknown repository id."); + } + } +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/fdo/PIDApiImpl.java b/application/src/main/java/com/indiscale/fdo/manager/service/fdo/PIDApiImpl.java new file mode 100644 index 0000000..f50d9c4 --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/fdo/PIDApiImpl.java @@ -0,0 +1,44 @@ +package com.indiscale.fdo.manager.service.fdo; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import com.indiscale.fdo.api.DigitalObject; +import com.indiscale.fdo.api.Manager; +import com.indiscale.fdo.manager.service.BaseController; +import com.indiscale.fdo.manager.service.ManagerPool; +import com.indiscale.fdo.manager.service.api.model.Links; +import com.indiscale.fdo.manager.service.api.model.ResolvePID200Response; +import com.indiscale.fdo.manager.service.api.operation.PidApi; + +@RestController +@CrossOrigin(origins = {"${react-dev-server}"}) +public class PIDApiImpl extends BaseController implements PidApi { + + @Autowired + ManagerPool managerPool; + + @Override + public ResponseEntity<ResolvePID200Response> resolvePID(String pid) { + try(Manager manager = managerPool.getManager()) { + DigitalObject resolved = manager.resolvePID(pid); + if (resolved == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Not found. Could not resolve PID."); + } + + return ResponseEntity.ok(createResponse(resolved)); + } + } + + private ResolvePID200Response createResponse(DigitalObject resolved) { + com.indiscale.fdo.manager.service.api.model.DigitalObject data = new com.indiscale.fdo.manager.service.api.model.DigitalObject(resolved.getPID(),resolved.isFDO()); + Links self = linkSelf(getOperations(getClass()).resolvePID(resolved.getPID())); + + return new ResolvePID200Response().data(data).links(self); + } + +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/hello/HelloImpl.java b/application/src/main/java/com/indiscale/fdo/manager/service/hello/HelloImpl.java index 2ff052e..c36de32 100644 --- a/application/src/main/java/com/indiscale/fdo/manager/service/hello/HelloImpl.java +++ b/application/src/main/java/com/indiscale/fdo/manager/service/hello/HelloImpl.java @@ -13,7 +13,7 @@ public class HelloImpl implements HelloApi { @Override public ResponseEntity<Hello> hello() { Hello hello = new Hello(); - hello.message("Hello from Spring Boot"); + hello.message("Hello from FDO Manager Service!"); return ResponseEntity.ok(hello); } diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/hello/InfoImpl.java b/application/src/main/java/com/indiscale/fdo/manager/service/hello/InfoImpl.java new file mode 100644 index 0000000..d631d31 --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/hello/InfoImpl.java @@ -0,0 +1,27 @@ +package com.indiscale.fdo.manager.service.hello; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RestController; + +import com.indiscale.fdo.manager.service.BaseController; +import com.indiscale.fdo.manager.service.api.model.GetInfo200Response; +import com.indiscale.fdo.manager.service.api.model.Info; +import com.indiscale.fdo.manager.service.api.operation.InfoApi; + +@RestController +@CrossOrigin(origins = {"${react-dev-server}"}) +public class InfoImpl extends BaseController implements InfoApi { + + @Value("${fdo.service.version}") + private String fdoServiceVersion; + @Value("${fdo.sdk.version}") + private String fdoSdkVersion; + + @Override + public ResponseEntity<GetInfo200Response> getInfo() { + Info data = new Info().fdoServiceVersion(fdoServiceVersion).fdoSdkVersion(fdoSdkVersion); + return ResponseEntity.ok(new GetInfo200Response(data)); + } +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/mock/MockManager.java b/application/src/main/java/com/indiscale/fdo/manager/service/mock/MockManager.java new file mode 100644 index 0000000..7f00e0e --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/mock/MockManager.java @@ -0,0 +1,48 @@ +package com.indiscale.fdo.manager.service.mock; + +import java.util.HashMap; +import java.util.Map; + +import com.indiscale.fdo.DefaultManager; +import com.indiscale.fdo.DefaultRepositoryRegistry; +import com.indiscale.fdo.api.Data; +import com.indiscale.fdo.api.DigitalObject; +import com.indiscale.fdo.api.FDO; +import com.indiscale.fdo.api.Metadata; +import com.indiscale.fdo.api.RepositoryConnection; +import com.indiscale.fdo.api.UnknownRepositoryTypeException; + +public class MockManager extends DefaultManager { + + private static Map<String, DigitalObject> pidRegistry = new HashMap<>(); + + public MockManager() { + MockRepositoryFactory mock = new MockRepositoryFactory(); + DefaultRepositoryRegistry registry = this.getRepositoryRegistry(); + registry.registerRepositoryType(mock); + try { + registry.registerRepository(mock.createMockConfig("mock-repo-1")); + registry.registerRepository(mock.createMockConfig("mock-repo-2")); + registry.registerRepository(mock.createMockConfig("mock-repo-3")); + } catch (UnknownRepositoryTypeException e) { + e.printStackTrace(); + } + } + + @Override + public FDO createFDO(RepositoryConnection repository, Data data, Metadata metadata) { + FDO fdo = super.createFDO(repository, data, metadata); + synchronized (pidRegistry) { + pidRegistry.put(fdo.getPID(), fdo); + } + return fdo; + } + + @Override + public DigitalObject resolvePID(String pid) { + synchronized (pidRegistry) { + return pidRegistry.get(pid); + } + } + +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/mock/MockRepositoryFactory.java b/application/src/main/java/com/indiscale/fdo/manager/service/mock/MockRepositoryFactory.java new file mode 100644 index 0000000..818c50e --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/mock/MockRepositoryFactory.java @@ -0,0 +1,97 @@ +package com.indiscale.fdo.manager.service.mock; + +import java.util.UUID; + +import com.indiscale.fdo.api.Data; +import com.indiscale.fdo.api.FDO; +import com.indiscale.fdo.api.Metadata; +import com.indiscale.fdo.api.RepositoryConnection; +import com.indiscale.fdo.api.RepositoryConfig; +import com.indiscale.fdo.api.RepositoryConnectionFactory; +import com.indiscale.fdo.api.RepositoryType; + +class MockRepository implements RepositoryConnection { + + public static class MockFDO implements FDO { + + private final String pid; + + public MockFDO() { + pid = UUID.randomUUID().toString(); + } + + @Override + public String getPID() { + return pid; + } + + } + private final String id; + private final RepositoryType type; + + public MockRepository(RepositoryConfig config) { + this.id = config.getId(); + this.type = config.getType(); + } + + @Override + public String getId() { + return id; + } + + @Override + public FDO createFDO(Data data, Metadata metadata) { + return new MockFDO(); + } + + @Override + public RepositoryType getType() { + return type; + } + + @Override + public void close() throws Exception { + } + +} + +public class MockRepositoryFactory implements RepositoryConnectionFactory { + + static class MockRepositoryConfig implements RepositoryConfig { + + private final String id; + + public MockRepositoryConfig(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + + @Override + public RepositoryType getType() { + return TYPE; + } + + } + + + public static final RepositoryType TYPE = new RepositoryType("MockDOIP").description("A mock-up for a DOIP repository."); + + @Override + public RepositoryConnection createConnection(RepositoryConfig config) { + return new MockRepository(config); + } + + @Override + public RepositoryType getType() { + return TYPE; + } + + public RepositoryConfig createMockConfig(String id) { + return new MockRepositoryConfig(id); + } + +} diff --git a/application/src/main/java/com/indiscale/fdo/manager/service/repositories/RepositoriesImpl.java b/application/src/main/java/com/indiscale/fdo/manager/service/repositories/RepositoriesImpl.java new file mode 100644 index 0000000..e0bd551 --- /dev/null +++ b/application/src/main/java/com/indiscale/fdo/manager/service/repositories/RepositoriesImpl.java @@ -0,0 +1,101 @@ +package com.indiscale.fdo.manager.service.repositories; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import com.indiscale.fdo.api.Manager; +import com.indiscale.fdo.api.RepositoryConfig; +import com.indiscale.fdo.api.UnknownRepositoryException; +import com.indiscale.fdo.manager.service.BaseController; +import com.indiscale.fdo.manager.service.ManagerPool; +import com.indiscale.fdo.manager.service.api.model.GetRepository200Response; +import com.indiscale.fdo.manager.service.api.model.Links; +import com.indiscale.fdo.manager.service.api.model.ListRepositories200Response; +import com.indiscale.fdo.manager.service.api.operation.RepositoriesApi; + +@RestController +@CrossOrigin(origins = {"${react-dev-server}"}) +public class RepositoriesImpl extends BaseController implements RepositoriesApi { + + static class Repository extends com.indiscale.fdo.manager.service.api.model.Repository { + private static final long serialVersionUID = -4753402740372027632L; + + public Repository(RepositoryConfig config) { + this.setId(config.getId()); + } + + String getSelfRel() { + Link selfLink = linkTo(methodOn(RepositoriesImpl.class).getRepository(this.getId())).withSelfRel(); + return selfLink.getHref(); + } + + String getCollectionRel() { + Link collectionLink = linkTo(methodOn(RepositoriesImpl.class).listRepositories()).withRel(IanaLinkRelations.COLLECTION); + return collectionLink.getHref(); + } + + public Repository withSelfRel() { + if (this.getLinks() == null) { + this.setLinks(new Links()); + } + this.getLinks().self(getSelfRel()); + return this; + } + + public Repository withCollectionRel() { + if (this.getLinks() == null) { + this.setLinks(new Links()); + } + this.getLinks().collection(getCollectionRel()); + return this; + } + } + + private ManagerPool factory; + + public RepositoriesImpl(ManagerPool factory) { + super(); + this.factory = factory; + } + + @Override + public ResponseEntity<ListRepositories200Response> listRepositories() { + try (Manager manager = factory.getManager()) { + List<com.indiscale.fdo.manager.service.api.model.Repository> results = new LinkedList<>(); + + List<RepositoryConfig> repositories = manager.getRepositoryRegistry().listRepositories(); + for (RepositoryConfig repo : repositories) { + results.add(new Repository(repo).withSelfRel()); + } + + Link selfLink = linkTo(methodOn(getClass()).listRepositories()).withSelfRel(); + Links links = new Links(); + links.self(selfLink.getHref()); + return ResponseEntity.ok(new ListRepositories200Response().data(results).links(links)); + } + } + + @Override + public ResponseEntity<GetRepository200Response> getRepository(String repositoryId) { + + try (Manager manager = factory.getManager()) { + RepositoryConfig config = manager.getRepositoryRegistry().getRepositoryConfig(repositoryId); + + Repository repo = new Repository(config); + return ResponseEntity.ok(new GetRepository200Response().data(repo).links(new Links().self(repo.getSelfRel()).collection(repo.getCollectionRel()))); + } catch (UnknownRepositoryException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Unknown Repository ID: " + repositoryId); + } + } +} diff --git a/application/src/main/resources/application.properties b/application/src/main/resources/application.properties index 8341274..88c4a70 100644 --- a/application/src/main/resources/application.properties +++ b/application/src/main/resources/application.properties @@ -1,2 +1,8 @@ # accept cross origin for the react dev server react-dev-server=http://localhost:3000 +server.port=8080 +server.servlet.context-path=/api +fdo.service.version=@project.version@ +fdo.sdk.version=@fdo.sdk.version@ +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB diff --git a/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloEmbeddedServerTest.java b/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloEmbeddedServerTest.java index 90c467a..b03ef85 100644 --- a/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloEmbeddedServerTest.java +++ b/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloEmbeddedServerTest.java @@ -1,9 +1,9 @@ package com.indiscale.fdo.manager.service.hello; -import com.indiscale.fdo.manager.service.hello.HelloImpl; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; @@ -24,6 +24,9 @@ class HelloEmbeddedServerTest { @Autowired private TestRestTemplate restTemplate; + @Value("${server.servlet.context-path}") + private String contextPath; + @Test void index() { Assertions.assertThat(hello).isNotNull(); @@ -31,6 +34,6 @@ class HelloEmbeddedServerTest { @Test void indexResultTest() { - Assertions.assertThat(restTemplate.getForObject("http://localhost:" + port + "/hello", String.class)).contains("Hello"); + Assertions.assertThat(restTemplate.getForObject("http://localhost:" + port + contextPath + "/hello", String.class)).contains("Hello"); } } diff --git a/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloMockMvcTest.java b/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloMockMvcTest.java index e59cfaf..a12e0b7 100644 --- a/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloMockMvcTest.java +++ b/application/src/test/java/com/indiscale/fdo/manager/service/hello/HelloMockMvcTest.java @@ -1,16 +1,15 @@ package com.indiscale.fdo.manager.service.hello; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; - /** * Test HelloImpl class (without embedded server). */ diff --git a/application/src/test/java/com/indiscale/fdo/manager/service/repositories/RepositoriesTest.java b/application/src/test/java/com/indiscale/fdo/manager/service/repositories/RepositoriesTest.java new file mode 100644 index 0000000..6d29c76 --- /dev/null +++ b/application/src/test/java/com/indiscale/fdo/manager/service/repositories/RepositoriesTest.java @@ -0,0 +1,42 @@ +package com.indiscale.fdo.manager.service.repositories; + + + + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; + +/** + * Test the {@link RepositoriesImpl} class + */ + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class RepositoriesTest { + + @Autowired + private RepositoriesImpl repositories; + + @LocalServerPort + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Value("${server.servlet.context-path}") + private String contextPath; + + @Test + void index() { + Assertions.assertThat(repositories).isNotNull(); + } + + @Test + void indexResultTest() { + Assertions.assertThat(restTemplate.getForObject("http://localhost:" + port + contextPath + "/repositories", String.class)).contains("data"); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index d74a0b8..1b37a39 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>3.2.1</version> + <version>3.2.2</version> </parent> <groupId>com.indiscale.fdo</groupId> -- GitLab