From db1704670af788f2e7f6aa84f38f0abfb5f642b1 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Mon, 24 Jul 2023 14:30:02 +0200
Subject: [PATCH] rename ids; implement file download

---
 api-lib-core/pom.xml                          |  2 +-
 .../java/de/ozgcloud/apilib/file/FileId.java  | 14 ---
 .../de/ozgcloud/apilib/file/OzgCloudFile.java |  2 +-
 .../ozgcloud/apilib/file/OzgCloudFileId.java  | 14 +++
 .../apilib/file/OzgCloudFileService.java      | 10 ++
 .../file/grpc/GrpcOzgCloudFileService.java    | 95 +++++++++++++++++++
 .../OzgCloudFileDownloadStreamObserver.java   | 41 ++++++++
 .../apilib/file/grpc/OzgCloudFileMapper.java  | 15 +++
 .../ozgcloud/apilib/user/OzgCloudUserId.java  | 14 +++
 .../java/de/ozgcloud/apilib/user/UserId.java  | 14 ---
 .../apilib/vorgang/OzgCloudVorgangHeader.java |  6 +-
 .../apilib/vorgang/OzgCloudVorgangStatus.java | 14 +++
 .../apilib/vorgang/VorgangStatus.java         | 14 ---
 .../vorgang/dummy/DummyVorgangService.java    |  6 +-
 .../vorgang/grpc/OzgCloudVorgangMapper.java   | 12 +--
 .../apilib/file/OzgCloudFileTestFactory.java  | 23 +++++
 .../GrpcFindFilesResponseTestFactory.java     | 15 +++
 .../grpc/GrpcOzgCloudFileServiceTest.java     | 95 +++++++++++++++++++
 .../file/grpc/GrpcOzgFileTestFactory.java     | 19 ++++
 .../OzgCloudVorgangHeaderTestFactory.java     |  6 +-
 api-lib-demo/pom.xml                          |  2 +-
 ozg-cloud-spring-boot-starter/pom.xml         |  2 +-
 pom.xml                                       |  2 +-
 23 files changed, 375 insertions(+), 62 deletions(-)
 delete mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/file/FileId.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileId.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileService.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileService.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileDownloadStreamObserver.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileMapper.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/user/OzgCloudUserId.java
 delete mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/user/UserId.java
 create mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangStatus.java
 delete mode 100644 api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/VorgangStatus.java
 create mode 100644 api-lib-core/src/test/java/de/ozgcloud/apilib/file/OzgCloudFileTestFactory.java
 create mode 100644 api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcFindFilesResponseTestFactory.java
 create mode 100644 api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileServiceTest.java
 create mode 100644 api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgFileTestFactory.java

diff --git a/api-lib-core/pom.xml b/api-lib-core/pom.xml
index 41dcf5e..18560a6 100644
--- a/api-lib-core/pom.xml
+++ b/api-lib-core/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>de.ozgcloud.api-lib</groupId>
 		<artifactId>api-lib-parent</artifactId>
-		<version>0.1.0-SNAPSHOT</version>
+		<version>0.2.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>api-lib-core</artifactId>
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/FileId.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/FileId.java
deleted file mode 100644
index 6cb26d1..0000000
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/FileId.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.ozgcloud.apilib.file;
-
-import de.itvsh.kop.common.datatype.StringBasedValue;
-
-public class FileId extends StringBasedValue {
-
-	FileId(String id) {
-		super(id);
-	}
-
-	public FileId from(String id) {
-		return new FileId(id);
-	}
-}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFile.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFile.java
index 61f97f6..1464584 100644
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFile.java
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFile.java
@@ -9,7 +9,7 @@ import lombok.ToString;
 @ToString
 public class OzgCloudFile {
 
-	private FileId id;
+	private OzgCloudFileId id;
 
 	private String name;
 	private String contentType;
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileId.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileId.java
new file mode 100644
index 0000000..d0f0d5b
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileId.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.apilib.file;
+
+import de.itvsh.kop.common.datatype.StringBasedValue;
+
+public class OzgCloudFileId extends StringBasedValue {
+
+	public OzgCloudFileId(String id) {
+		super(id);
+	}
+
+	public static OzgCloudFileId from(String id) {
+		return new OzgCloudFileId(id);
+	}
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileService.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileService.java
new file mode 100644
index 0000000..dc5f385
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/OzgCloudFileService.java
@@ -0,0 +1,10 @@
+package de.ozgcloud.apilib.file;
+
+import java.io.OutputStream;
+
+public interface OzgCloudFileService {
+
+	OzgCloudFile getFile(OzgCloudFileId id);
+
+	void writeFileDataToStream(OzgCloudFileId id, OutputStream streamToWriteData);
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileService.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileService.java
new file mode 100644
index 0000000..3fc5f89
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileService.java
@@ -0,0 +1,95 @@
+package de.ozgcloud.apilib.file.grpc;
+
+import java.io.OutputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import de.itvsh.kop.common.binaryfile.FileId;
+import de.itvsh.kop.common.errorhandling.TechnicalException;
+import de.itvsh.ozg.pluto.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceBlockingStub;
+import de.itvsh.ozg.pluto.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceStub;
+import de.itvsh.ozg.pluto.grpc.binaryFile.GrpcBinaryFilesRequest;
+import de.itvsh.ozg.pluto.grpc.binaryFile.GrpcFindFilesResponse;
+import de.itvsh.ozg.pluto.grpc.binaryFile.GrpcGetBinaryFileDataRequest;
+import de.itvsh.ozg.pluto.grpc.file.GrpcOzgFile;
+import de.ozgcloud.apilib.errorhandling.NotFoundException;
+import de.ozgcloud.apilib.file.OzgCloudFile;
+import de.ozgcloud.apilib.file.OzgCloudFileId;
+import de.ozgcloud.apilib.file.OzgCloudFileService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.java.Log;
+import net.devh.boot.grpc.client.inject.GrpcClient;
+
+@Service
+@ConditionalOnProperty("ozgcloud.file-manager.address")
+@RequiredArgsConstructor
+@Log
+public class GrpcOzgCloudFileService implements OzgCloudFileService {
+
+	@GrpcClient("file-manager")
+	private final BinaryFileServiceBlockingStub blockingStub;
+	@GrpcClient("file-manager")
+	private final BinaryFileServiceStub asyncServiceStub;
+
+	@Autowired
+	private final OzgCloudFileMapper mapper;
+
+	static final int CHUNK_SIZE = 255 * 1024;
+
+	@Override
+	public OzgCloudFile getFile(OzgCloudFileId id) {
+		var response = blockingStub.findBinaryFilesMetaData(buildFindFileRequest(id));
+
+		return mapper.fromGrpc(getSingleResponse(response, id));
+	}
+
+	GrpcBinaryFilesRequest buildFindFileRequest(OzgCloudFileId id) {
+		return GrpcBinaryFilesRequest.newBuilder()
+				.addFileId(id.toString())
+				.build();
+	}
+
+	GrpcOzgFile getSingleResponse(GrpcFindFilesResponse response, OzgCloudFileId id) {
+		var responseCount = response.getFileCount();
+		if (responseCount == 0) {
+			throw new NotFoundException(id, "OzgCloudFile");
+		}
+		if (responseCount > 1) {
+			LOG.log(Level.WARNING, () -> "Found more then one file with id '%s'. Returning first one, only.".formatted(id.toString()));
+		}
+		return response.getFile(0);
+	}
+
+	@Override
+	public void writeFileDataToStream(OzgCloudFileId id, OutputStream streamToWriteData) {
+		var fileId = FileId.from(id.toString());
+		var streamFuture = new CompletableFuture<Boolean>();
+		var responseObserver = new OzgCloudFileDownloadStreamObserver(streamFuture, streamToWriteData);
+
+		asyncServiceStub.getBinaryFileContent(buildGrpcGetBinaryFileDataRequest(fileId), responseObserver);
+
+		waitUntilFutureToComplete(streamFuture);
+	}
+
+	private GrpcGetBinaryFileDataRequest buildGrpcGetBinaryFileDataRequest(FileId fileId) {
+		return GrpcGetBinaryFileDataRequest.newBuilder().setFileId(fileId.toString()).build();
+	}
+
+	void waitUntilFutureToComplete(CompletableFuture<Boolean> streamFuture) {
+		try {
+			streamFuture.get(10, TimeUnit.MINUTES);
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+			throw new TechnicalException(e.getMessage(), e);
+		} catch (ExecutionException | TimeoutException e) {
+			throw new TechnicalException(e.getMessage(), e);
+		}
+	}
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileDownloadStreamObserver.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileDownloadStreamObserver.java
new file mode 100644
index 0000000..3a9826d
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileDownloadStreamObserver.java
@@ -0,0 +1,41 @@
+package de.ozgcloud.apilib.file.grpc;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.CompletableFuture;
+
+import de.itvsh.kop.common.errorhandling.TechnicalException;
+import de.itvsh.ozg.pluto.grpc.binaryFile.GrpcGetBinaryFileDataResponse;
+import io.grpc.stub.StreamObserver;
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
+class OzgCloudFileDownloadStreamObserver implements StreamObserver<GrpcGetBinaryFileDataResponse> {
+
+	private final CompletableFuture<Boolean> streamFuture;
+	private final OutputStream out;
+
+	@Override
+	public void onNext(GrpcGetBinaryFileDataResponse response) {
+		writeToStream(response.getFileContent().toByteArray());
+	}
+
+	void writeToStream(byte[] contentPart) {
+		try {
+			out.write(contentPart);
+		} catch (IOException e) {
+			throw new TechnicalException("Download file error writing on OutputStream", e);
+		}
+	}
+
+	@Override
+	public void onError(Throwable t) {
+		streamFuture.completeExceptionally(t);
+	}
+
+	@Override
+	public void onCompleted() {
+		streamFuture.complete(true);
+	}
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileMapper.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileMapper.java
new file mode 100644
index 0000000..f8e5c3b
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/file/grpc/OzgCloudFileMapper.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.apilib.file.grpc;
+
+import org.mapstruct.Mapper;
+
+import de.itvsh.ozg.pluto.grpc.file.GrpcOzgFile;
+import de.ozgcloud.apilib.file.OzgCloudFile;
+import de.ozgcloud.apilib.file.OzgCloudFileId;
+
+@Mapper
+public interface OzgCloudFileMapper {
+
+	OzgCloudFile fromGrpc(GrpcOzgFile grpcFile);
+
+	OzgCloudFileId toOzgCloudFileId(String string);
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/user/OzgCloudUserId.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/user/OzgCloudUserId.java
new file mode 100644
index 0000000..8c89abf
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/user/OzgCloudUserId.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.apilib.user;
+
+import de.itvsh.kop.common.datatype.StringBasedValue;
+
+public class OzgCloudUserId extends StringBasedValue {
+
+	OzgCloudUserId(String id) {
+		super(id);
+	}
+
+	public static OzgCloudUserId from(String id) {
+		return new OzgCloudUserId(id);
+	}
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/user/UserId.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/user/UserId.java
deleted file mode 100644
index a8d4e3f..0000000
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/user/UserId.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.ozgcloud.apilib.user;
-
-import de.itvsh.kop.common.datatype.StringBasedValue;
-
-public class UserId extends StringBasedValue {
-
-	UserId(String id) {
-		super(id);
-	}
-
-	public static UserId from(String id) {
-		return new UserId(id);
-	}
-}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeader.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeader.java
index a6af8c3..f8396cf 100644
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeader.java
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeader.java
@@ -2,7 +2,7 @@ package de.ozgcloud.apilib.vorgang;
 
 import java.time.ZonedDateTime;
 
-import de.ozgcloud.apilib.user.UserId;
+import de.ozgcloud.apilib.user.OzgCloudUserId;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.ToString;
@@ -11,11 +11,11 @@ import lombok.ToString;
 @Getter
 public class OzgCloudVorgangHeader {
 
-	private VorgangStatus status;
+	private OzgCloudVorgangStatus status;
 
 	private ZonedDateTime createdAt;
 
-	private UserId assignedTo;
+	private OzgCloudUserId assignedTo;
 
 	private String aktenzeichen;
 
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangStatus.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangStatus.java
new file mode 100644
index 0000000..2a3080d
--- /dev/null
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangStatus.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.apilib.vorgang;
+
+import de.itvsh.kop.common.datatype.StringBasedValue;
+
+public class OzgCloudVorgangStatus extends StringBasedValue {
+
+	OzgCloudVorgangStatus(String status) {
+		super(status);
+	}
+
+	public static OzgCloudVorgangStatus from(String status) {
+		return new OzgCloudVorgangStatus(status);
+	}
+}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/VorgangStatus.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/VorgangStatus.java
deleted file mode 100644
index 5babf8a..0000000
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/VorgangStatus.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.ozgcloud.apilib.vorgang;
-
-import de.itvsh.kop.common.datatype.StringBasedValue;
-
-public class VorgangStatus extends StringBasedValue {
-
-	VorgangStatus(String status) {
-		super(status);
-	}
-
-	public static VorgangStatus from(String status) {
-		return new VorgangStatus(status);
-	}
-}
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/dummy/DummyVorgangService.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/dummy/DummyVorgangService.java
index 5e31042..761d910 100644
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/dummy/DummyVorgangService.java
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/dummy/DummyVorgangService.java
@@ -15,9 +15,9 @@ import de.ozgcloud.apilib.vorgang.OzgCloudVorgang;
 import de.ozgcloud.apilib.vorgang.OzgCloudVorgangHeader;
 import de.ozgcloud.apilib.vorgang.OzgCloudVorgangId;
 import de.ozgcloud.apilib.vorgang.OzgCloudVorgangService;
+import de.ozgcloud.apilib.vorgang.OzgCloudVorgangStatus;
 import de.ozgcloud.apilib.vorgang.OzgCloudVorgangStub;
 import de.ozgcloud.apilib.vorgang.Page;
-import de.ozgcloud.apilib.vorgang.VorgangStatus;
 
 @Service
 @ConditionalOnMissingBean(OzgCloudVorgangService.class)
@@ -29,7 +29,7 @@ public class DummyVorgangService implements OzgCloudVorgangService {
 	private static final String VORGANG_NAME_1 = "Antrag auf einen Waffelschein";
 	private static final String VORGANG_NR_1 = "1234";
 	private static final OzgCloudVorgangHeader HEADER_1 = OzgCloudVorgangHeader.builder()
-			.status(VorgangStatus.from("NEU"))
+			.status(OzgCloudVorgangStatus.from("NEU"))
 			.createdAt(ZonedDateTime.now())
 			.aktenzeichen("ABC-123-04")
 			.build();
@@ -38,7 +38,7 @@ public class DummyVorgangService implements OzgCloudVorgangService {
 	private static final String VORGANG_NAME_2 = "Antrag auf Führung eines Kampfhamsters";
 	private static final String VORGANG_NR_2 = "5678";
 	private static final OzgCloudVorgangHeader HEADER_2 = OzgCloudVorgangHeader.builder()
-			.status(VorgangStatus.from("IN_BEARBEITUNG"))
+			.status(OzgCloudVorgangStatus.from("IN_BEARBEITUNG"))
 			.createdAt(ZonedDateTime.now().minus(1, ChronoUnit.DAYS))
 			.aktenzeichen("ABC-546-12")
 			.build();
diff --git a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/grpc/OzgCloudVorgangMapper.java b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/grpc/OzgCloudVorgangMapper.java
index 007daf0..3973f79 100644
--- a/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/grpc/OzgCloudVorgangMapper.java
+++ b/api-lib-core/src/main/java/de/ozgcloud/apilib/vorgang/grpc/OzgCloudVorgangMapper.java
@@ -6,11 +6,11 @@ import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 
 import de.itvsh.ozg.pluto.vorgang.GrpcVorgangWithEingang;
-import de.ozgcloud.apilib.user.UserId;
+import de.ozgcloud.apilib.user.OzgCloudUserId;
 import de.ozgcloud.apilib.vorgang.OzgCloudServiceKontoType;
 import de.ozgcloud.apilib.vorgang.OzgCloudVorgang;
 import de.ozgcloud.apilib.vorgang.OzgCloudVorgangIdMapper;
-import de.ozgcloud.apilib.vorgang.VorgangStatus;
+import de.ozgcloud.apilib.vorgang.OzgCloudVorgangStatus;
 
 @Mapper(uses = { OzgCloudEingangMapper.class, OzgCloudVorgangIdMapper.class })
 public interface OzgCloudVorgangMapper {
@@ -28,11 +28,11 @@ public interface OzgCloudVorgangMapper {
 		return Optional.ofNullable(type).map(OzgCloudServiceKontoType::from).orElse(null);
 	}
 
-	default UserId toUserId(String userId) {
-		return Optional.ofNullable(userId).map(UserId::from).orElse(null);
+	default OzgCloudUserId toUserId(String userId) {
+		return Optional.ofNullable(userId).map(OzgCloudUserId::from).orElse(null);
 	}
 
-	default VorgangStatus toVorgangId(String status) {
-		return Optional.ofNullable(status).map(VorgangStatus::from).orElse(null);
+	default OzgCloudVorgangStatus toVorgangId(String status) {
+		return Optional.ofNullable(status).map(OzgCloudVorgangStatus::from).orElse(null);
 	}
 }
diff --git a/api-lib-core/src/test/java/de/ozgcloud/apilib/file/OzgCloudFileTestFactory.java b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/OzgCloudFileTestFactory.java
new file mode 100644
index 0000000..04b41dc
--- /dev/null
+++ b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/OzgCloudFileTestFactory.java
@@ -0,0 +1,23 @@
+package de.ozgcloud.apilib.file;
+
+import java.util.UUID;
+
+public class OzgCloudFileTestFactory {
+
+	public static final OzgCloudFileId ID = OzgCloudFileId.from(UUID.randomUUID().toString());
+	public static final String NAME = "testfile.pdf";
+	public static final String CONTENT_TYPE = "application/pdf";
+	public static final long SIZE = 1024;
+
+	public static OzgCloudFile create() {
+		return createBuilder().build();
+	}
+
+	public static OzgCloudFile.OzgCloudFileBuilder createBuilder() {
+		return OzgCloudFile.builder()
+				.id(ID)
+				.name(NAME)
+				.contentType(CONTENT_TYPE)
+				.size(SIZE);
+	}
+}
diff --git a/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcFindFilesResponseTestFactory.java b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcFindFilesResponseTestFactory.java
new file mode 100644
index 0000000..d64013a
--- /dev/null
+++ b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcFindFilesResponseTestFactory.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.apilib.file.grpc;
+
+import de.itvsh.ozg.pluto.grpc.binaryFile.GrpcFindFilesResponse;
+
+public class GrpcFindFilesResponseTestFactory {
+
+	public static GrpcFindFilesResponse create() {
+		return createBuilder().build();
+	}
+
+	public static GrpcFindFilesResponse.Builder createBuilder() {
+		return GrpcFindFilesResponse.newBuilder()
+				.addFile(GrpcOzgFileTestFactory.create());
+	}
+}
diff --git a/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileServiceTest.java b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileServiceTest.java
new file mode 100644
index 0000000..ddc2304
--- /dev/null
+++ b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgCloudFileServiceTest.java
@@ -0,0 +1,95 @@
+package de.ozgcloud.apilib.file.grpc;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.itvsh.ozg.pluto.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceBlockingStub;
+import de.itvsh.ozg.pluto.grpc.binaryFile.GrpcFindFilesResponse;
+import de.ozgcloud.apilib.errorhandling.NotFoundException;
+import de.ozgcloud.apilib.file.OzgCloudFileTestFactory;
+
+class GrpcOzgCloudFileServiceTest {
+
+	@InjectMocks
+	private GrpcOzgCloudFileService service;
+
+	@Mock
+	private BinaryFileServiceBlockingStub blockingStub;
+	@Spy
+	private OzgCloudFileMapper mapper = Mappers.getMapper(OzgCloudFileMapper.class);
+
+	@Nested
+	class TestGetFile {
+
+		@Nested
+		class CallGetFile {
+		@BeforeEach
+		void init() {
+			when(blockingStub.findBinaryFilesMetaData(any())).thenReturn(GrpcFindFilesResponse.newBuilder().addFile(GrpcOzgFileTestFactory.create()).build());
+		}
+
+			@Test
+			void shouldCallStub() {
+				service.getFile(OzgCloudFileTestFactory.ID);
+
+				verify(blockingStub).findBinaryFilesMetaData(any());
+			}
+
+			@Test
+			void shouldCallMapper() {
+				service.getFile(OzgCloudFileTestFactory.ID);
+
+				verify(mapper).fromGrpc(any());
+			}
+		}
+
+		@Nested
+		class BuildFindFileRequest {
+
+			@Test
+			void shouldAddFileId() {
+				var request = service.buildFindFileRequest(OzgCloudFileTestFactory.ID);
+
+				assertThat(request.getFileIdList()).hasSize(1).first().isEqualTo(OzgCloudFileTestFactory.ID.toString());
+			}
+		}
+
+		@Nested
+		class GetSingleResponse {
+			@Test
+			void shouldReturnSingleFile() {
+				var result = service.getSingleResponse(GrpcFindFilesResponseTestFactory.create(), OzgCloudFileTestFactory.ID);
+
+				assertThat(result).usingRecursiveComparison().isEqualTo(GrpcOzgFileTestFactory.create());
+			}
+
+			@Test
+			void shouldReturnFirstFile() {
+				var response = GrpcFindFilesResponseTestFactory.createBuilder().addFile(GrpcOzgFileTestFactory.createBuilder().setId("2").build())
+						.build();
+
+				var result = service.getSingleResponse(response, OzgCloudFileTestFactory.ID);
+
+				assertThat(result).usingRecursiveComparison().isEqualTo(GrpcOzgFileTestFactory.create());
+			}
+
+			@Test
+			void shouldThrowNotFound() {
+				var response = GrpcFindFilesResponseTestFactory.createBuilder().clearFile().build();
+
+				assertThatThrownBy(() -> service.getSingleResponse(response, OzgCloudFileTestFactory.ID))
+						.isInstanceOf(NotFoundException.class);
+			}
+		}
+	}
+
+}
diff --git a/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgFileTestFactory.java b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgFileTestFactory.java
new file mode 100644
index 0000000..c7f9179
--- /dev/null
+++ b/api-lib-core/src/test/java/de/ozgcloud/apilib/file/grpc/GrpcOzgFileTestFactory.java
@@ -0,0 +1,19 @@
+package de.ozgcloud.apilib.file.grpc;
+
+import de.itvsh.ozg.pluto.grpc.file.GrpcOzgFile;
+import de.ozgcloud.apilib.file.OzgCloudFileTestFactory;
+
+public class GrpcOzgFileTestFactory {
+
+	static GrpcOzgFile create() {
+		return createBuilder().build();
+	}
+
+	static GrpcOzgFile.Builder createBuilder() {
+		return GrpcOzgFile.newBuilder()
+				.setId(OzgCloudFileTestFactory.ID.toString())
+				.setName(OzgCloudFileTestFactory.NAME)
+				.setContentType(OzgCloudFileTestFactory.CONTENT_TYPE)
+				.setSize(OzgCloudFileTestFactory.SIZE);
+	}
+}
diff --git a/api-lib-core/src/test/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeaderTestFactory.java b/api-lib-core/src/test/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeaderTestFactory.java
index 94a01f7..3af1c6b 100644
--- a/api-lib-core/src/test/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeaderTestFactory.java
+++ b/api-lib-core/src/test/java/de/ozgcloud/apilib/vorgang/OzgCloudVorgangHeaderTestFactory.java
@@ -3,14 +3,14 @@ package de.ozgcloud.apilib.vorgang;
 import java.time.ZonedDateTime;
 import java.util.UUID;
 
-import de.ozgcloud.apilib.user.UserId;
+import de.ozgcloud.apilib.user.OzgCloudUserId;
 
 public class OzgCloudVorgangHeaderTestFactory {
 
-	public static VorgangStatus STATUS = VorgangStatus.from("WORKING");
+	public static OzgCloudVorgangStatus STATUS = OzgCloudVorgangStatus.from("WORKING");
 	public static String CREATED_AT_STR = "2023-04-01T10:00:15Z";
 	public static ZonedDateTime CREATED_AT = ZonedDateTime.parse(CREATED_AT_STR);
-	public static UserId ASSIGNED_TO = UserId.from(UUID.randomUUID().toString());
+	public static OzgCloudUserId ASSIGNED_TO = OzgCloudUserId.from(UUID.randomUUID().toString());
 	public static String AKTENZEICHEN = "DE_12345_XY";
 
 	public static OzgCloudVorgangHeader create() {
diff --git a/api-lib-demo/pom.xml b/api-lib-demo/pom.xml
index 64e08d0..53dc4c2 100644
--- a/api-lib-demo/pom.xml
+++ b/api-lib-demo/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>de.ozgcloud.api-lib</groupId>
 		<artifactId>api-lib-parent</artifactId>
-		<version>0.1.0-SNAPSHOT</version>
+		<version>0.2.0-SNAPSHOT</version>
 	</parent>
 	<artifactId>api-lib-demo</artifactId>
 
diff --git a/ozg-cloud-spring-boot-starter/pom.xml b/ozg-cloud-spring-boot-starter/pom.xml
index a75449d..16b2d2c 100644
--- a/ozg-cloud-spring-boot-starter/pom.xml
+++ b/ozg-cloud-spring-boot-starter/pom.xml
@@ -5,7 +5,7 @@
 	<parent>
 		<groupId>de.ozgcloud.api-lib</groupId>
 		<artifactId>api-lib-parent</artifactId>
-		<version>0.1.0-SNAPSHOT</version>
+		<version>0.2.0-SNAPSHOT</version>
 	</parent>
 	<artifactId>ozg-cloud-spring-boot-starter</artifactId>
 
diff --git a/pom.xml b/pom.xml
index 78abf2c..d27fbb9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
 
 	<groupId>de.ozgcloud.api-lib</groupId>
 	<artifactId>api-lib-parent</artifactId>
-	<version>0.1.0-SNAPSHOT</version>
+	<version>0.2.0-SNAPSHOT</version>
 	<description>Library project to use the OZG-Cloud API</description>
 
 	<packaging>pom</packaging>
-- 
GitLab