From 4960695d1fc0d231a5ccbfcfe722e2b6929a10fb Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Thu, 23 Jan 2025 10:56:25 +0100
Subject: [PATCH 1/7] OZG-7262 OZG-7566 Notify callObserver about error

---
 .../GrpcBinaryFileServerDownloader.java       |  2 +-
 .../GrpcBinaryFileServerDownloaderTest.java   | 31 ++++++-------------
 2 files changed, 10 insertions(+), 23 deletions(-)

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index b992f42..cb4a208 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -143,7 +143,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 		} catch (Exception e) {
 			closeOutputStream();
 			closeInputStream();
-			throw new TechnicalException("Error occurred during downloading file content download.", e);
+			callObserver.onError(new TechnicalException("Error occurred during downloading file content download.", e));
 		}
 	}
 
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
index 6050e6b..483e38d 100644
--- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
+++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
@@ -26,7 +26,6 @@ package de.ozgcloud.common.binaryfile;
 import com.google.protobuf.ByteString;
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.common.test.ReflectionTestUtils;
-import io.grpc.Context;
 import io.grpc.stub.CallStreamObserver;
 import lombok.SneakyThrows;
 import org.junit.jupiter.api.BeforeEach;
@@ -407,11 +406,6 @@ class GrpcBinaryFileServerDownloaderTest {
 
 			verify(callObserver).onCompleted();
 		}
-
-		@SneakyThrows
-		private boolean getRequestFinished() {
-			return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get();
-		}
 	}
 
 	@DisplayName("Handle safety")
@@ -436,20 +430,10 @@ class GrpcBinaryFileServerDownloaderTest {
 				setOutputStreamField(outputStream);
 			}
 
-			@SneakyThrows
-			@Test
-			void shouldThrowTechnicalException() {
-				assertThatThrownBy(this::handleSafety).isInstanceOf(TechnicalException.class).extracting(Throwable::getCause).isEqualTo(exception);
-			}
-
 			@SneakyThrows
 			@Test
 			void shouldCloseOutputStream() {
-				try {
-					handleSafety();
-				} catch (Exception e) {
-					// do nothing
-				}
+				handleSafety();
 
 				verify(outputStream).close();
 			}
@@ -457,15 +441,18 @@ class GrpcBinaryFileServerDownloaderTest {
 			@SneakyThrows
 			@Test
 			void shouldCloseInputStream() {
-				try {
-					handleSafety();
-				} catch (Exception e) {
-					// do nothing
-				}
+				handleSafety();
 
 				verify(inputStream).close();
 			}
 
+			@Test
+			void shouldNotifyCallObserver() {
+				handleSafety();
+
+				verify(callObserver).onError(argThat(TechnicalException.class::isInstance));
+			}
+
 			private void handleSafety() {
 				downloader.handleSafety(this::dummyMethodThrowingException);
 			}
-- 
GitLab


From d1187a9ec9c5a4c3a9f0e6c5a6dd0f2e29fba6e3 Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Mon, 27 Jan 2025 11:11:27 +0100
Subject: [PATCH 2/7] OZG-7262 OZG-7566 Fix error handling

---
 .../BinaryFileDownloadException.java          |  33 +++
 .../binaryfile/CallStreamObserverWrapper.java |  78 ++++++
 .../GrpcBinaryFileServerDownloader.java       |  49 ++--
 .../CallStreamObserverWrapperTest.java        | 254 ++++++++++++++++++
 .../GrpcBinaryFileServerDownloaderTest.java   | 223 +++++++++------
 5 files changed, 535 insertions(+), 102 deletions(-)
 create mode 100644 ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java
 create mode 100644 ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java
 create mode 100644 ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java
new file mode 100644
index 0000000..9a5d462
--- /dev/null
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.common.binaryfile;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+
+class BinaryFileDownloadException extends TechnicalException {
+
+	public BinaryFileDownloadException(Throwable cause) {
+		super("Error occurred during downloading file content", cause);
+	}
+}
diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java
new file mode 100644
index 0000000..a0462c6
--- /dev/null
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.common.binaryfile;
+
+import java.util.function.Supplier;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import io.grpc.stub.CallStreamObserver;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+	class CallStreamObserverWrapper<V> {
+
+	private final CallStreamObserver<V> callStreamObserver;
+	private boolean failed;
+
+	public void setOnReadyHandler(Runnable onReadyHandler) {
+		callStreamObserver.setOnReadyHandler(onReadyHandler);
+	}
+
+	public synchronized boolean isReady() {
+		return ifNotFailed(callStreamObserver::isReady);
+	}
+
+	public synchronized void onNext(V value) {
+		ifNotFailed(() -> callStreamObserver.onNext(value));
+	}
+
+	public synchronized void onError(Throwable t) {
+		if (!failed) {
+			callStreamObserver.onError(new BinaryFileDownloadException(t));
+			failed = true;
+		} else {
+			handleIllegalCallAfterError();
+		}
+	}
+
+	public synchronized void onCompleted() {
+		ifNotFailed(callStreamObserver::onCompleted);
+	}
+
+	private void ifNotFailed(Runnable runnable) {
+		if (!failed) {
+			runnable.run();
+		} else {
+			handleIllegalCallAfterError();
+		}
+	}
+
+	private <T> T ifNotFailed(Supplier<T> supplier) {
+		return !failed ? supplier.get() : handleIllegalCallAfterError();
+	}
+
+	private <T> T handleIllegalCallAfterError() {
+		throw new TechnicalException("CallStreamObserver called after error");
+	}
+}
diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index cb4a208..44f3fae 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -23,14 +23,6 @@
  */
 package de.ozgcloud.common.binaryfile;
 
-import com.google.protobuf.ByteString;
-import de.ozgcloud.common.errorhandling.TechnicalException;
-import io.grpc.stub.CallStreamObserver;
-import lombok.Builder;
-import lombok.extern.log4j.Log4j2;
-import org.apache.commons.io.IOUtils;
-import org.springframework.core.task.TaskExecutor;
-
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PipedInputStream;
@@ -39,12 +31,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
+import org.apache.commons.io.IOUtils;
+import org.springframework.core.task.TaskExecutor;
+
+import com.google.protobuf.ByteString;
+
+import io.grpc.stub.CallStreamObserver;
+import lombok.Builder;
+import lombok.extern.log4j.Log4j2;
+
 @Log4j2
 public class GrpcBinaryFileServerDownloader<T> {
 
 	private static final int CHUNK_SIZE = 255 * 1024;
 
-	private final CallStreamObserver<T> callObserver;
+	private final CallStreamObserverWrapper<T> callObserver;
 	private final Function<ByteString, T> chunkBuilder;
 	private final Consumer<OutputStream> downloadConsumer;
 	private final TaskExecutor taskExecutor;
@@ -60,7 +61,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 	@Builder
 	public GrpcBinaryFileServerDownloader(CallStreamObserver<T> callObserver, Function<ByteString, T> chunkBuilder,
 			Consumer<OutputStream> downloadConsumer, TaskExecutor taskExecutor) {
-		this.callObserver = callObserver;
+		this.callObserver = new CallStreamObserverWrapper<>(callObserver);
 		this.chunkBuilder = chunkBuilder;
 		this.downloadConsumer = downloadConsumer;
 		this.taskExecutor = taskExecutor;
@@ -78,11 +79,20 @@ public class GrpcBinaryFileServerDownloader<T> {
 
 	void doStart() {
 		LOG.debug("Starting download.");
-		handleSafety(this::setupStreams);
+		safelySetupStreams();
 		taskExecutor.execute(this::startDownload);
 		callObserver.setOnReadyHandler(this::sendChunks);
 	}
 
+	void safelySetupStreams() {
+		try {
+			setupStreams();
+		} catch (Exception e) {
+			closeStreams();
+			throw new BinaryFileDownloadException(e);
+		}
+	}
+
 	void setupStreams() throws IOException {
 		outputStream = new PipedOutputStream();
 		inputStream = new PipedInputStream(GrpcBinaryFileServerDownloader.CHUNK_SIZE);
@@ -90,7 +100,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 	}
 
 	void startDownload() {
-		handleSafety(this::doDownload);
+		withDownloadErrorHandling(this::doDownload);
 	}
 
 	void doDownload() {
@@ -103,7 +113,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 	}
 
 	synchronized void sendChunks() {
-		handleSafety(this::doSendChunks);
+		withDownloadErrorHandling(this::doSendChunks);
 	}
 
 	void doSendChunks() throws IOException {
@@ -137,16 +147,20 @@ public class GrpcBinaryFileServerDownloader<T> {
 		callObserver.onCompleted();
 	}
 
-	void handleSafety(ExceptionalRunnable runnable) {
+	void withDownloadErrorHandling(ExceptionalRunnable runnable) {
 		try {
 			runnable.run();
 		} catch (Exception e) {
-			closeOutputStream();
-			closeInputStream();
-			callObserver.onError(new TechnicalException("Error occurred during downloading file content download.", e));
+			closeStreams();
+			callObserver.onError(e);
 		}
 	}
 
+	private void closeStreams() {
+		closeOutputStream();
+		closeInputStream();
+	}
+
 	void closeOutputStream() {
 		IOUtils.closeQuietly(outputStream, e -> LOG.error("OutputStream cannot be closed.", e));
 	}
@@ -154,5 +168,4 @@ public class GrpcBinaryFileServerDownloader<T> {
 	void closeInputStream() {
 		IOUtils.closeQuietly(inputStream, e -> LOG.error("InputStream cannot be closed.", e));
 	}
-
 }
\ No newline at end of file
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java
new file mode 100644
index 0000000..c1d5999
--- /dev/null
+++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.common.binaryfile;
+
+import static org.assertj.core.api.Assertions.*;
+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.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.common.test.ReflectionTestUtils;
+import io.grpc.stub.CallStreamObserver;
+
+class CallStreamObserverWrapperTest {
+
+	@Mock
+	private CallStreamObserver<GrpcResponseDummy> callObserver;
+	@InjectMocks
+	private CallStreamObserverWrapper<GrpcResponseDummy> wrapper;
+
+	@Nested
+	class TestSetOnReadyHandler {
+
+		@Test
+		void shouldForwardToObserver() {
+			var onReadyHandler = mock(Runnable.class);
+
+			wrapper.setOnReadyHandler(onReadyHandler);
+
+			verify(callObserver).setOnReadyHandler(onReadyHandler);
+		}
+	}
+
+	@Nested
+	class TestIsReady {
+
+		@Nested
+		class OnFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", true);
+			}
+
+			@Test
+			void shouldNotCallObserver() {
+				catchThrowable(wrapper::isReady);
+
+				verifyNoInteractions(callObserver);
+			}
+
+			@Test
+			void shouldThrowException() {
+				assertThatThrownBy(wrapper::isReady).isInstanceOf(TechnicalException.class);
+			}
+		}
+
+		@Nested
+		class OnNotFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", false);
+			}
+
+			@Test
+			void shouldForwardToObserver() {
+				wrapper.isReady();
+
+				verify(callObserver).isReady();
+			}
+
+			@ParameterizedTest
+			@ValueSource(booleans = { true, false })
+			void shouldReturnObserverResult(boolean observerResult) {
+				when(callObserver.isReady()).thenReturn(observerResult);
+
+				var ready = wrapper.isReady();
+
+				assertThat(ready).isEqualTo(observerResult);
+			}
+		}
+	}
+
+	@Nested
+	class TestOnNext {
+
+		private final GrpcResponseDummy grpccResponse = new GrpcResponseDummy();
+
+		@Nested
+		class OnFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", true);
+			}
+
+			@Test
+			void shouldNotCallObserver() {
+				catchThrowable(() -> wrapper.onNext(grpccResponse));
+
+				verifyNoInteractions(callObserver);
+			}
+
+			@Test
+			void shouldThrowException() {
+				assertThatThrownBy(() -> wrapper.onNext(grpccResponse)).isInstanceOf(TechnicalException.class);
+			}
+		}
+
+		@Nested
+		class OnNotFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", false);
+			}
+
+			@Test
+			void shouldForwardToObserver() {
+				wrapper.onNext(grpccResponse);
+
+				verify(callObserver).onNext(grpccResponse);
+			}
+		}
+	}
+
+	@Nested
+	class TestOnError {
+
+		private final TechnicalException exception = new TechnicalException("dummy");
+
+		@Nested
+		class OnFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", true);
+			}
+
+			@Test
+			void shouldNotCallObserver() {
+				catchThrowable(() -> wrapper.onError(exception));
+
+				verifyNoInteractions(callObserver);
+			}
+
+			@Test
+			void shouldThrowException() {
+				assertThatThrownBy(() -> wrapper.onError(exception)).isInstanceOf(TechnicalException.class);
+			}
+		}
+
+		@Nested
+		class OnNotFailed {
+
+			private ArgumentCaptor<BinaryFileDownloadException> captor = ArgumentCaptor.forClass(BinaryFileDownloadException.class);
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", false);
+			}
+
+			@Test
+			void shouldForwardBinaryFileDownloadExceptionToObserver() {
+				wrapper.onError(exception);
+
+				verify(callObserver).onError(captor.capture());
+				assertThat(captor.getValue()).isInstanceOf(BinaryFileDownloadException.class);
+				assertThat(captor.getValue().getCause()).isEqualTo(exception);
+			}
+
+			@Test
+			void shouldSetFailedToTrue() {
+				wrapper.onError(exception);
+
+				assertThat(wrapper).extracting("failed").isEqualTo(true);
+			}
+		}
+	}
+
+	@Nested
+	class TestOnCompleted {
+
+		@Nested
+		class OnFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", true);
+			}
+
+			@Test
+			void shouldNotCallObserver() {
+				catchThrowable(wrapper::onCompleted);
+
+				verifyNoInteractions(callObserver);
+			}
+
+			@Test
+			void shouldThrowException() {
+				assertThatThrownBy(wrapper::onCompleted).isInstanceOf(TechnicalException.class);
+			}
+		}
+
+		@Nested
+		class OnNotFailed {
+
+			@BeforeEach
+			void init() {
+				ReflectionTestUtils.setField(wrapper, "failed", false);
+			}
+
+			@Test
+			void shouldForwardToObserver() {
+				wrapper.onCompleted();
+
+				verify(callObserver).onCompleted();
+			}
+		}
+	}
+
+	private static class GrpcResponseDummy {
+
+	}
+}
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
index 483e38d..e4f265d 100644
--- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
+++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
@@ -23,19 +23,9 @@
  */
 package de.ozgcloud.common.binaryfile;
 
-import com.google.protobuf.ByteString;
-import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.common.test.ReflectionTestUtils;
-import io.grpc.stub.CallStreamObserver;
-import lombok.SneakyThrows;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.springframework.core.task.TaskExecutor;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -47,14 +37,25 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-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.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.springframework.core.task.TaskExecutor;
+
+import com.google.protobuf.ByteString;
+
+import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.common.test.ReflectionTestUtils;
+import lombok.SneakyThrows;
 
 class GrpcBinaryFileServerDownloaderTest {
 
 	@Mock
-	private CallStreamObserver<GrpcResponseDummy> callObserver;
+	private CallStreamObserverWrapper<GrpcResponseDummy> callObserverWrapper;
 	@Mock
 	private Function<ByteString, GrpcResponseDummy> chunkBuilder;
 	@Mock
@@ -66,11 +67,11 @@ class GrpcBinaryFileServerDownloaderTest {
 
 	@BeforeEach
 	void init() {
-		downloader = spy(GrpcBinaryFileServerDownloader.<GrpcResponseDummy>builder().callObserver(callObserver).downloadConsumer(downloadConsumer)
+		downloader = spy(GrpcBinaryFileServerDownloader.<GrpcResponseDummy>builder().downloadConsumer(downloadConsumer)
 				.chunkBuilder(chunkBuilder).taskExecutor(taskExecutor).build());
+		ReflectionTestUtils.setField(downloader, "callObserver", callObserverWrapper);
 	}
 
-	@DisplayName("Start")
 	@Nested
 	class TestStart {
 
@@ -116,25 +117,23 @@ class GrpcBinaryFileServerDownloaderTest {
 		}
 	}
 
-	@DisplayName("do start")
 	@Nested
 	class TestDoStart {
 
 		@Captor
 		private ArgumentCaptor<Runnable> runnableCaptor;
-		@Captor
-		private ArgumentCaptor<ExceptionalRunnable> setupStreamCaptor;
+
+		@BeforeEach
+		void init() {
+			doNothing().when(downloader).safelySetupStreams();
+		}
 
 		@SneakyThrows
 		@Test
-		void shouldCallSetupStreams() {
-			doNothing().when(downloader).handleSafety(any());
-
+		void shouldSafelySetupStreams() {
 			downloader.doStart();
 
-			verify(downloader).handleSafety(setupStreamCaptor.capture());
-			setupStreamCaptor.getValue().run();
-			verify(downloader).setupStreams();
+			verify(downloader).safelySetupStreams();
 		}
 
 		@Test
@@ -153,12 +152,69 @@ class GrpcBinaryFileServerDownloaderTest {
 		void shouldSetOnReadyHandler() {
 			downloader.doStart();
 
-			verify(callObserver).setOnReadyHandler(runnableCaptor.capture());
+			verify(callObserverWrapper).setOnReadyHandler(runnableCaptor.capture());
 			assertThat(runnableCaptor.getValue()).isNotNull();
 		}
 	}
 
-	@DisplayName("Start download")
+	@Nested
+	class TestSafelySetupStreams {
+
+		@SneakyThrows
+		@Test
+		void shouldSetupStreams() {
+			doNothing().when(downloader).setupStreams();
+
+			safelySetupStreams();
+
+			verify(downloader).setupStreams();
+		}
+
+		@Nested
+		class OnException {
+
+			@Mock
+			private PipedOutputStream outputStream;
+			@Mock
+			private PipedInputStream inputStream;
+
+			private final IOException exception = new IOException();
+
+			@SneakyThrows
+			@BeforeEach
+			void init() {
+				setInputStreamField(inputStream);
+				setOutputStreamField(outputStream);
+				doThrow(exception).when(downloader).setupStreams();
+			}
+
+			@Test
+			void shouldThrowBinaryFileDownloadException() {
+				assertThatThrownBy(() -> downloader.safelySetupStreams()).isInstanceOf(BinaryFileDownloadException.class).hasCause(exception);
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCloseOutputStream() {
+				catchThrowable(TestSafelySetupStreams.this::safelySetupStreams);
+
+				verify(outputStream).close();
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCloseInputStream() {
+				catchThrowable(TestSafelySetupStreams.this::safelySetupStreams);
+
+				verify(inputStream).close();
+			}
+		}
+
+		private void safelySetupStreams() {
+			downloader.safelySetupStreams();
+		}
+	}
+
 	@Nested
 	class TestStartDownload {
 
@@ -168,71 +224,64 @@ class GrpcBinaryFileServerDownloaderTest {
 		@SneakyThrows
 		@Test
 		void shouldCallDoDownload() {
-			doNothing().when(downloader).handleSafety(any());
+			doNothing().when(downloader).withDownloadErrorHandling(any());
 			doNothing().when(downloader).doDownload();
 
 			downloader.startDownload();
 
-			verify(downloader).handleSafety(runnableCaptor.capture());
+			verify(downloader).withDownloadErrorHandling(runnableCaptor.capture());
 			runnableCaptor.getValue().run();
 			verify(downloader).doDownload();
 		}
-	}
 
-	@DisplayName("do")
-	@Nested
-	class TestDoDownload {
+		@Nested
+		class TestDoDownload {
 
-		@Mock
-		private PipedOutputStream outputStream;
+			@Mock
+			private PipedOutputStream outputStream;
 
-		@BeforeEach
-		void mock() {
-			setOutputStreamField(outputStream);
-		}
+			@BeforeEach
+			void mock() {
+				setOutputStreamField(outputStream);
+			}
 
-		@SneakyThrows
-		@Test
-		void shouldCallDownloadConsumer() {
-			downloader.doDownload();
+			@SneakyThrows
+			@Test
+			void shouldCallDownloadConsumer() {
+				downloader.doDownload();
 
-			verify(downloadConsumer).accept(outputStream);
-		}
+				verify(downloadConsumer).accept(outputStream);
+			}
 
-		@SneakyThrows
-		@Test
-		void shouldCloseOutputStream() {
-			downloader.doDownload();
+			@SneakyThrows
+			@Test
+			void shouldCloseOutputStream() {
+				downloader.doDownload();
 
-			verify(outputStream).close();
+				verify(outputStream).close();
+			}
 		}
 	}
 
-	@DisplayName("Send chunks")
 	@Nested
 	class TestSendChunks {
 
-		@SneakyThrows
-		@Test
-		void shouldCallHandleSafety() {
-			doNothing().when(downloader).doSendChunks();
-
-			downloader.sendChunks();
-
-			verify(downloader).handleSafety(any(ExceptionalRunnable.class));
-		}
+		@Captor
+		private ArgumentCaptor<ExceptionalRunnable> runnableCaptor;
 
 		@SneakyThrows
 		@Test
-		void shouldCallDoDownload() {
+		void shouldCallWithDownloadErrorHandling() {
+			doNothing().when(downloader).withDownloadErrorHandling(any());
 			doNothing().when(downloader).doSendChunks();
 
 			downloader.sendChunks();
 
+			verify(downloader).withDownloadErrorHandling(runnableCaptor.capture());
+			runnableCaptor.getValue().run();
 			verify(downloader).doSendChunks();
 		}
 
-		@DisplayName("do")
 		@Nested
 		class TestDoSendChunks {
 
@@ -249,7 +298,7 @@ class GrpcBinaryFileServerDownloaderTest {
 			@BeforeEach
 			void mock() {
 				doNothing().when(downloader).tryCompleteRequest();
-				when(callObserver.isReady()).thenReturn(true);
+				when(callObserverWrapper.isReady()).thenReturn(true);
 				when(inputStream.read(any())).thenReturn(readBytes, -1);
 				setInputStreamField(inputStream);
 				new Random().nextBytes(buffer);
@@ -271,7 +320,7 @@ class GrpcBinaryFileServerDownloaderTest {
 
 				doSendChunks();
 
-				verify(callObserver).onNext(grpcResponseDummy);
+				verify(callObserverWrapper).onNext(grpcResponseDummy);
 			}
 
 			@DisplayName("should call complete grpc stream if download has finished and stream has no data left")
@@ -404,28 +453,35 @@ class GrpcBinaryFileServerDownloaderTest {
 		void shouldCallOnCompleted() {
 			downloader.completeRequest();
 
-			verify(callObserver).onCompleted();
+			verify(callObserverWrapper).onCompleted();
 		}
 	}
 
-	@DisplayName("Handle safety")
 	@Nested
-	class TestHandleSafety {
+	class TestWithDownloadErrorHandling {
+
+		@SneakyThrows
+		@Test
+		void shouldRunRunnable() {
+			var runnable = mock(ExceptionalRunnable.class);
+
+			downloader.withDownloadErrorHandling(runnable);
+
+			verify(runnable).run();
+		}
 
-		@DisplayName("on exception")
 		@Nested
-		class TestOnException {
+		class OnException {
 
 			@Mock
 			private PipedOutputStream outputStream;
 			@Mock
 			private PipedInputStream inputStream;
 
-			private final IOException exception = new IOException();
+			private final TechnicalException exception = new TechnicalException("dummy");
 
-			@SneakyThrows
 			@BeforeEach
-			void mock() {
+			void init() {
 				setInputStreamField(inputStream);
 				setOutputStreamField(outputStream);
 			}
@@ -433,7 +489,7 @@ class GrpcBinaryFileServerDownloaderTest {
 			@SneakyThrows
 			@Test
 			void shouldCloseOutputStream() {
-				handleSafety();
+				withDownloadErrorHandling();
 
 				verify(outputStream).close();
 			}
@@ -441,27 +497,26 @@ class GrpcBinaryFileServerDownloaderTest {
 			@SneakyThrows
 			@Test
 			void shouldCloseInputStream() {
-				handleSafety();
+				withDownloadErrorHandling();
 
 				verify(inputStream).close();
 			}
 
 			@Test
 			void shouldNotifyCallObserver() {
-				handleSafety();
+				withDownloadErrorHandling();
 
-				verify(callObserver).onError(argThat(TechnicalException.class::isInstance));
+				verify(callObserverWrapper).onError(argThat(TechnicalException.class::isInstance));
 			}
 
-			private void handleSafety() {
-				downloader.handleSafety(this::dummyMethodThrowingException);
+			private void withDownloadErrorHandling() {
+				downloader.withDownloadErrorHandling(this::dummyMethodThrowingException);
 			}
 
-			private void dummyMethodThrowingException() throws IOException {
+			private void dummyMethodThrowingException() throws TechnicalException {
 				throw exception;
 			}
 		}
-
 	}
 
 	private void setOutputStreamField(OutputStream outputStream) {
@@ -484,6 +539,6 @@ class GrpcBinaryFileServerDownloaderTest {
 		return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get();
 	}
 
-	static class GrpcResponseDummy {
+	private static class GrpcResponseDummy {
 	}
 }
\ No newline at end of file
-- 
GitLab


From da68d4a4899bd1822aaf24c641b2c35a6603c732 Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Wed, 29 Jan 2025 15:55:51 +0100
Subject: [PATCH 3/7] OZG-7262 OZG-7627 Remove CallStreamObserverWrapper

---
 .../binaryfile/CallStreamObserverWrapper.java |  78 ----
 .../GrpcBinaryFileServerDownloader.java       |  55 ++-
 .../CallStreamObserverWrapperTest.java        | 254 ------------
 .../GrpcBinaryFileServerDownloaderTest.java   | 390 ++++++++++++------
 4 files changed, 303 insertions(+), 474 deletions(-)
 delete mode 100644 ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java
 delete mode 100644 ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java
deleted file mode 100644
index a0462c6..0000000
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapper.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.common.binaryfile;
-
-import java.util.function.Supplier;
-
-import de.ozgcloud.common.errorhandling.TechnicalException;
-import io.grpc.stub.CallStreamObserver;
-import lombok.RequiredArgsConstructor;
-
-@RequiredArgsConstructor
-	class CallStreamObserverWrapper<V> {
-
-	private final CallStreamObserver<V> callStreamObserver;
-	private boolean failed;
-
-	public void setOnReadyHandler(Runnable onReadyHandler) {
-		callStreamObserver.setOnReadyHandler(onReadyHandler);
-	}
-
-	public synchronized boolean isReady() {
-		return ifNotFailed(callStreamObserver::isReady);
-	}
-
-	public synchronized void onNext(V value) {
-		ifNotFailed(() -> callStreamObserver.onNext(value));
-	}
-
-	public synchronized void onError(Throwable t) {
-		if (!failed) {
-			callStreamObserver.onError(new BinaryFileDownloadException(t));
-			failed = true;
-		} else {
-			handleIllegalCallAfterError();
-		}
-	}
-
-	public synchronized void onCompleted() {
-		ifNotFailed(callStreamObserver::onCompleted);
-	}
-
-	private void ifNotFailed(Runnable runnable) {
-		if (!failed) {
-			runnable.run();
-		} else {
-			handleIllegalCallAfterError();
-		}
-	}
-
-	private <T> T ifNotFailed(Supplier<T> supplier) {
-		return !failed ? supplier.get() : handleIllegalCallAfterError();
-	}
-
-	private <T> T handleIllegalCallAfterError() {
-		throw new TechnicalException("CallStreamObserver called after error");
-	}
-}
diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index 44f3fae..090a53d 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -28,6 +28,7 @@ import java.io.OutputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -45,15 +46,16 @@ public class GrpcBinaryFileServerDownloader<T> {
 
 	private static final int CHUNK_SIZE = 255 * 1024;
 
-	private final CallStreamObserverWrapper<T> callObserver;
+	private final CallStreamObserver<T> callObserver;
 	private final Function<ByteString, T> chunkBuilder;
 	private final Consumer<OutputStream> downloadConsumer;
 	private final TaskExecutor taskExecutor;
 
-	private final byte[] buffer = new byte[GrpcBinaryFileServerDownloader.CHUNK_SIZE];
+	private final byte[] buffer = new byte[CHUNK_SIZE];
 	private final AtomicBoolean started = new AtomicBoolean(false);
 	private final AtomicBoolean downloadFinished = new AtomicBoolean(false);
 	private final AtomicBoolean requestFinished = new AtomicBoolean(false);
+	private final AtomicReference<BinaryFileDownloadException> error = new AtomicReference<>();
 
 	private PipedInputStream inputStream;
 	private PipedOutputStream outputStream;
@@ -61,7 +63,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 	@Builder
 	public GrpcBinaryFileServerDownloader(CallStreamObserver<T> callObserver, Function<ByteString, T> chunkBuilder,
 			Consumer<OutputStream> downloadConsumer, TaskExecutor taskExecutor) {
-		this.callObserver = new CallStreamObserverWrapper<>(callObserver);
+		this.callObserver = callObserver;
 		this.chunkBuilder = chunkBuilder;
 		this.downloadConsumer = downloadConsumer;
 		this.taskExecutor = taskExecutor;
@@ -88,19 +90,27 @@ public class GrpcBinaryFileServerDownloader<T> {
 		try {
 			setupStreams();
 		} catch (Exception e) {
-			closeStreams();
+			closeOutputStream();
+			closeInputStream();
 			throw new BinaryFileDownloadException(e);
 		}
 	}
 
 	void setupStreams() throws IOException {
 		outputStream = new PipedOutputStream();
-		inputStream = new PipedInputStream(GrpcBinaryFileServerDownloader.CHUNK_SIZE);
+		inputStream = new PipedInputStream(CHUNK_SIZE);
 		outputStream.connect(inputStream);
 	}
 
 	void startDownload() {
-		withDownloadErrorHandling(this::doDownload);
+		try {
+			doDownload();
+		} catch (Exception e) {
+			error.set(new BinaryFileDownloadException(e));
+		} finally {
+			closeOutputStream();
+			sendChunks();
+		}
 	}
 
 	void doDownload() {
@@ -108,12 +118,14 @@ public class GrpcBinaryFileServerDownloader<T> {
 		downloadConsumer.accept(outputStream);
 		LOG.debug("Download completed.");
 		downloadFinished.set(true);
-		closeOutputStream();
-		sendChunks();
 	}
 
 	synchronized void sendChunks() {
-		withDownloadErrorHandling(this::doSendChunks);
+		try {
+			doSendChunks();
+		} catch (Exception e) {
+			completeRequestWithError(e);
+		}
 	}
 
 	void doSendChunks() throws IOException {
@@ -121,7 +133,11 @@ public class GrpcBinaryFileServerDownloader<T> {
 			return;
 		}
 		int bytesRead;
-		while (callObserver.isReady()) {
+		while (isReady()) {
+			if (error.get() != null) {
+				completeRequestWithError(error.get());
+				break;
+			}
 			if ((bytesRead = inputStream.read(buffer)) == -1) {
 				tryCompleteRequest();
 				break;
@@ -131,6 +147,10 @@ public class GrpcBinaryFileServerDownloader<T> {
 		}
 	}
 
+	private boolean isReady() {
+		return callObserver.isReady();
+	}
+
 	void tryCompleteRequest() {
 		if (shouldCompleteRequest()) {
 			completeRequest();
@@ -147,18 +167,11 @@ public class GrpcBinaryFileServerDownloader<T> {
 		callObserver.onCompleted();
 	}
 
-	void withDownloadErrorHandling(ExceptionalRunnable runnable) {
-		try {
-			runnable.run();
-		} catch (Exception e) {
-			closeStreams();
-			callObserver.onError(e);
-		}
-	}
-
-	private void closeStreams() {
-		closeOutputStream();
+	void completeRequestWithError(Throwable t) {
+		LOG.debug("Complete download request with error", t);
+		requestFinished.set(true);
 		closeInputStream();
+		callObserver.onError(t);
 	}
 
 	void closeOutputStream() {
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java
deleted file mode 100644
index c1d5999..0000000
--- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/CallStreamObserverWrapperTest.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.common.binaryfile;
-
-import static org.assertj.core.api.Assertions.*;
-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.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-
-import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.common.test.ReflectionTestUtils;
-import io.grpc.stub.CallStreamObserver;
-
-class CallStreamObserverWrapperTest {
-
-	@Mock
-	private CallStreamObserver<GrpcResponseDummy> callObserver;
-	@InjectMocks
-	private CallStreamObserverWrapper<GrpcResponseDummy> wrapper;
-
-	@Nested
-	class TestSetOnReadyHandler {
-
-		@Test
-		void shouldForwardToObserver() {
-			var onReadyHandler = mock(Runnable.class);
-
-			wrapper.setOnReadyHandler(onReadyHandler);
-
-			verify(callObserver).setOnReadyHandler(onReadyHandler);
-		}
-	}
-
-	@Nested
-	class TestIsReady {
-
-		@Nested
-		class OnFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", true);
-			}
-
-			@Test
-			void shouldNotCallObserver() {
-				catchThrowable(wrapper::isReady);
-
-				verifyNoInteractions(callObserver);
-			}
-
-			@Test
-			void shouldThrowException() {
-				assertThatThrownBy(wrapper::isReady).isInstanceOf(TechnicalException.class);
-			}
-		}
-
-		@Nested
-		class OnNotFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", false);
-			}
-
-			@Test
-			void shouldForwardToObserver() {
-				wrapper.isReady();
-
-				verify(callObserver).isReady();
-			}
-
-			@ParameterizedTest
-			@ValueSource(booleans = { true, false })
-			void shouldReturnObserverResult(boolean observerResult) {
-				when(callObserver.isReady()).thenReturn(observerResult);
-
-				var ready = wrapper.isReady();
-
-				assertThat(ready).isEqualTo(observerResult);
-			}
-		}
-	}
-
-	@Nested
-	class TestOnNext {
-
-		private final GrpcResponseDummy grpccResponse = new GrpcResponseDummy();
-
-		@Nested
-		class OnFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", true);
-			}
-
-			@Test
-			void shouldNotCallObserver() {
-				catchThrowable(() -> wrapper.onNext(grpccResponse));
-
-				verifyNoInteractions(callObserver);
-			}
-
-			@Test
-			void shouldThrowException() {
-				assertThatThrownBy(() -> wrapper.onNext(grpccResponse)).isInstanceOf(TechnicalException.class);
-			}
-		}
-
-		@Nested
-		class OnNotFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", false);
-			}
-
-			@Test
-			void shouldForwardToObserver() {
-				wrapper.onNext(grpccResponse);
-
-				verify(callObserver).onNext(grpccResponse);
-			}
-		}
-	}
-
-	@Nested
-	class TestOnError {
-
-		private final TechnicalException exception = new TechnicalException("dummy");
-
-		@Nested
-		class OnFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", true);
-			}
-
-			@Test
-			void shouldNotCallObserver() {
-				catchThrowable(() -> wrapper.onError(exception));
-
-				verifyNoInteractions(callObserver);
-			}
-
-			@Test
-			void shouldThrowException() {
-				assertThatThrownBy(() -> wrapper.onError(exception)).isInstanceOf(TechnicalException.class);
-			}
-		}
-
-		@Nested
-		class OnNotFailed {
-
-			private ArgumentCaptor<BinaryFileDownloadException> captor = ArgumentCaptor.forClass(BinaryFileDownloadException.class);
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", false);
-			}
-
-			@Test
-			void shouldForwardBinaryFileDownloadExceptionToObserver() {
-				wrapper.onError(exception);
-
-				verify(callObserver).onError(captor.capture());
-				assertThat(captor.getValue()).isInstanceOf(BinaryFileDownloadException.class);
-				assertThat(captor.getValue().getCause()).isEqualTo(exception);
-			}
-
-			@Test
-			void shouldSetFailedToTrue() {
-				wrapper.onError(exception);
-
-				assertThat(wrapper).extracting("failed").isEqualTo(true);
-			}
-		}
-	}
-
-	@Nested
-	class TestOnCompleted {
-
-		@Nested
-		class OnFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", true);
-			}
-
-			@Test
-			void shouldNotCallObserver() {
-				catchThrowable(wrapper::onCompleted);
-
-				verifyNoInteractions(callObserver);
-			}
-
-			@Test
-			void shouldThrowException() {
-				assertThatThrownBy(wrapper::onCompleted).isInstanceOf(TechnicalException.class);
-			}
-		}
-
-		@Nested
-		class OnNotFailed {
-
-			@BeforeEach
-			void init() {
-				ReflectionTestUtils.setField(wrapper, "failed", false);
-			}
-
-			@Test
-			void shouldForwardToObserver() {
-				wrapper.onCompleted();
-
-				verify(callObserver).onCompleted();
-			}
-		}
-	}
-
-	private static class GrpcResponseDummy {
-
-	}
-}
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
index e4f265d..2e45282 100644
--- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
+++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
@@ -34,6 +34,7 @@ import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -50,12 +51,13 @@ import com.google.protobuf.ByteString;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.common.test.ReflectionTestUtils;
+import io.grpc.stub.CallStreamObserver;
 import lombok.SneakyThrows;
 
 class GrpcBinaryFileServerDownloaderTest {
 
 	@Mock
-	private CallStreamObserverWrapper<GrpcResponseDummy> callObserverWrapper;
+	private CallStreamObserver<GrpcResponseDummy> callObserver;
 	@Mock
 	private Function<ByteString, GrpcResponseDummy> chunkBuilder;
 	@Mock
@@ -67,9 +69,8 @@ class GrpcBinaryFileServerDownloaderTest {
 
 	@BeforeEach
 	void init() {
-		downloader = spy(GrpcBinaryFileServerDownloader.<GrpcResponseDummy>builder().downloadConsumer(downloadConsumer)
+		downloader = spy(GrpcBinaryFileServerDownloader.<GrpcResponseDummy>builder().callObserver(callObserver).downloadConsumer(downloadConsumer)
 				.chunkBuilder(chunkBuilder).taskExecutor(taskExecutor).build());
-		ReflectionTestUtils.setField(downloader, "callObserver", callObserverWrapper);
 	}
 
 	@Nested
@@ -152,7 +153,7 @@ class GrpcBinaryFileServerDownloaderTest {
 		void shouldSetOnReadyHandler() {
 			downloader.doStart();
 
-			verify(callObserverWrapper).setOnReadyHandler(runnableCaptor.capture());
+			verify(callObserver).setOnReadyHandler(runnableCaptor.capture());
 			assertThat(runnableCaptor.getValue()).isNotNull();
 		}
 	}
@@ -218,126 +219,289 @@ class GrpcBinaryFileServerDownloaderTest {
 	@Nested
 	class TestStartDownload {
 
-		@Captor
-		private ArgumentCaptor<ExceptionalRunnable> runnableCaptor;
+		@Mock
+		private PipedOutputStream outputStream;
+
+		@BeforeEach
+		void init() {
+			setOutputStreamField(outputStream);
+		}
 
-		@SneakyThrows
 		@Test
-		void shouldCallDoDownload() {
-			doNothing().when(downloader).withDownloadErrorHandling(any());
-			doNothing().when(downloader).doDownload();
+		void shouldErrorBeInitiallyNull() {
+			assertThat(getError()).isNull();
+		}
 
-			downloader.startDownload();
+		@Nested
+		class OnNoException {
 
-			verify(downloader).withDownloadErrorHandling(runnableCaptor.capture());
-			runnableCaptor.getValue().run();
-			verify(downloader).doDownload();
+			@BeforeEach
+			void init() {
+				doNothing().when(downloader).doDownload();
+			}
+
+			@Test
+			void shouldDoDownload() {
+				downloader.startDownload();
+
+				verify(downloader).doDownload();
+			}
+
+			@SneakyThrows
+			@Test
+			void shouldCloseOutputStream() {
+				downloader.startDownload();
+
+				verify(outputStream).close();
+			}
+
+			@Test
+			@DisplayName("should send chunks here to not wait for callObserver to change its ready status")
+			void shouldSendChunks() {
+				downloader.startDownload();
+
+				verify(downloader).sendChunks();
+			}
 		}
 
 		@Nested
-		class TestDoDownload {
+		class OnException {
 
-			@Mock
-			private PipedOutputStream outputStream;
+			private final TechnicalException exception = new TechnicalException("error");
 
 			@BeforeEach
-			void mock() {
-				setOutputStreamField(outputStream);
+			void init() {
+				doThrow(exception).when(downloader).doDownload();
 			}
 
-			@SneakyThrows
 			@Test
-			void shouldCallDownloadConsumer() {
-				downloader.doDownload();
+			void shouldSetError() {
+				downloader.startDownload();
 
-				verify(downloadConsumer).accept(outputStream);
+				assertThat(getError()).isInstanceOf(BinaryFileDownloadException.class).hasCause(exception);
 			}
 
 			@SneakyThrows
 			@Test
 			void shouldCloseOutputStream() {
-				downloader.doDownload();
+				downloader.startDownload();
 
 				verify(outputStream).close();
 			}
+
+			@Test
+			@DisplayName("should send chunks here to communicate error to callObserver")
+			void shouldSendChunks() {
+				downloader.startDownload();
+
+				verify(downloader).sendChunks();
+			}
 		}
 	}
 
 	@Nested
-	class TestSendChunks {
+	class TestDoDownload {
 
-		@Captor
-		private ArgumentCaptor<ExceptionalRunnable> runnableCaptor;
+		@Mock
+		private PipedOutputStream outputStream;
+
+		@BeforeEach
+		void mock() {
+			setOutputStreamField(outputStream);
+		}
 
-		@SneakyThrows
 		@Test
-		void shouldCallWithDownloadErrorHandling() {
-			doNothing().when(downloader).withDownloadErrorHandling(any());
-			doNothing().when(downloader).doSendChunks();
+		void shouldCallDownloadConsumer() {
+			downloader.doDownload();
 
-			downloader.sendChunks();
+			verify(downloadConsumer).accept(outputStream);
+		}
 
-			verify(downloader).withDownloadErrorHandling(runnableCaptor.capture());
-			runnableCaptor.getValue().run();
-			verify(downloader).doSendChunks();
+		@Test
+		void shouldDownloadFinishedBeInitiallyFalse() {
+			assertThat(getDownloadFinished()).isFalse();
 		}
 
-		@Nested
-		class TestDoSendChunks {
+		@Test
+		void shouldSetDownloadFinished() {
+			downloader.doDownload();
 
-			@Mock
-			private PipedInputStream inputStream;
-			@Captor
-			private ArgumentCaptor<ByteString> byteStringCaptor;
+			assertThat(getDownloadFinished()).isTrue();
+		}
+	}
+
+	@Nested
+	class TestSendChunks {
 
-			private final int readBytes = 20;
-			private final byte[] buffer = new byte[readBytes];
-			private final GrpcResponseDummy grpcResponseDummy = new GrpcResponseDummy();
+		@Nested
+		class OnNoException {
 
 			@SneakyThrows
 			@BeforeEach
-			void mock() {
-				doNothing().when(downloader).tryCompleteRequest();
-				when(callObserverWrapper.isReady()).thenReturn(true);
-				when(inputStream.read(any())).thenReturn(readBytes, -1);
-				setInputStreamField(inputStream);
-				new Random().nextBytes(buffer);
-				ReflectionTestUtils.setField(downloader, "buffer", buffer);
+			void init() {
+				doNothing().when(downloader).doSendChunks();
 			}
 
+			@SneakyThrows
 			@Test
-			void shouldCallChunkBuilder() {
-				doSendChunks();
+			void shouldDoSendChunks() {
+				downloader.sendChunks();
 
-				verify(chunkBuilder).apply(byteStringCaptor.capture());
-				assertThat(byteStringCaptor.getValue().toByteArray()).isEqualTo(buffer);
+				verify(downloader).doSendChunks();
 			}
+		}
 
-			@DisplayName("should send next chunk if callObserver is ready and stream already received data")
-			@Test
-			void shouldCallOnNext() {
-				when(chunkBuilder.apply(any())).thenReturn(grpcResponseDummy);
+		@Nested
+		class OnException {
 
-				doSendChunks();
+			private final TechnicalException exception = new TechnicalException("error");
 
-				verify(callObserverWrapper).onNext(grpcResponseDummy);
+			@SneakyThrows
+			@BeforeEach
+			void init() {
+				doThrow(exception).when(downloader).doSendChunks();
 			}
 
-			@DisplayName("should call complete grpc stream if download has finished and stream has no data left")
 			@Test
-			void shouldCallCompleteDownload() {
-				setDownloadFinishedField(true);
+			void shouldCompleteRequestWithError() {
+				downloader.sendChunks();
 
+				verify(downloader).completeRequestWithError(exception);
+			}
+		}
+	}
+
+	@Nested
+	class TestDoSendChunks {
+
+		@Nested
+		class OnRequestFinished {
+
+			@BeforeEach
+			void init() {
+				setRequestFinishedField(true);
+			}
+
+			@Test
+			void shouldNotInteractWithCallObserver() {
 				doSendChunks();
 
-				verify(downloader).tryCompleteRequest();
+				verifyNoInteractions(callObserver);
 			}
+		}
 
-			@SneakyThrows
-			private void doSendChunks() {
-				downloader.doSendChunks();
+		@Nested
+		class OnRequestNotFinished {
+
+			@Nested
+			class OnNotReady {
+
+				@BeforeEach
+				void init() {
+					when(callObserver.isReady()).thenReturn(false);
+				}
+
+				@Test
+				void shouldOnlyCallIsReadyOnObserver() {
+					doSendChunks();
+
+					verify(callObserver).isReady();
+					verifyNoMoreInteractions(callObserver);
+				}
+			}
+
+			@Nested
+			class OnReady {
+
+				@BeforeEach
+				void init() {
+					when(callObserver.isReady()).thenReturn(true);
+				}
+
+				@Nested
+				class OnHasError {
+
+					private final BinaryFileDownloadException exception = new BinaryFileDownloadException(new TechnicalException("error"));
+
+					@BeforeEach
+					void init() {
+						setErrorField(exception);
+						doNothing().when(downloader).completeRequestWithError(any());
+					}
+
+					@Test
+					void shouldOnlyCallIsReadyOnObserver() {
+						doSendChunks();
+
+						verify(callObserver).isReady();
+						verifyNoMoreInteractions(callObserver);
+					}
+
+					@Test
+					void shouldCompleteRequestWithError() {
+						doSendChunks();
+
+						verify(downloader).completeRequestWithError(exception);
+					}
+				}
+
+				@Nested
+				class OnHasNoError {
+					@Mock
+					private PipedInputStream inputStream;
+					@Captor
+					private ArgumentCaptor<ByteString> byteStringCaptor;
+
+					private final int readBytes = 20;
+					private final byte[] buffer = new byte[readBytes];
+					private final GrpcResponseDummy grpcResponseDummy = new GrpcResponseDummy();
+
+					@SneakyThrows
+					@BeforeEach
+					void mock() {
+						doNothing().when(downloader).tryCompleteRequest();
+						when(callObserver.isReady()).thenReturn(true);
+						when(inputStream.read(any())).thenReturn(readBytes, -1);
+						setInputStreamField(inputStream);
+						new Random().nextBytes(buffer);
+						ReflectionTestUtils.setField(downloader, "buffer", buffer);
+					}
+
+					@Test
+					void shouldCallChunkBuilder() {
+						doSendChunks();
+
+						verify(chunkBuilder).apply(byteStringCaptor.capture());
+						assertThat(byteStringCaptor.getValue().toByteArray()).isEqualTo(buffer);
+					}
+
+					@DisplayName("should send next chunk if callObserver is ready and stream already received data")
+					@Test
+					void shouldCallOnNext() {
+						when(chunkBuilder.apply(any())).thenReturn(grpcResponseDummy);
+
+						doSendChunks();
+
+						verify(callObserver).onNext(grpcResponseDummy);
+					}
+
+					@DisplayName("should call complete grpc stream if download has finished and stream has no data left")
+					@Test
+					void shouldTryCompleteRequest() {
+						setDownloadFinishedField(true);
+
+						doSendChunks();
+
+						verify(downloader).tryCompleteRequest();
+					}
+				}
 			}
 		}
+
+		@SneakyThrows
+		private void doSendChunks() {
+			downloader.doSendChunks();
+		}
 	}
 
 	@Nested
@@ -453,69 +617,41 @@ class GrpcBinaryFileServerDownloaderTest {
 		void shouldCallOnCompleted() {
 			downloader.completeRequest();
 
-			verify(callObserverWrapper).onCompleted();
+			verify(callObserver).onCompleted();
 		}
 	}
 
 	@Nested
-	class TestWithDownloadErrorHandling {
-
-		@SneakyThrows
-		@Test
-		void shouldRunRunnable() {
-			var runnable = mock(ExceptionalRunnable.class);
+	class TestCompleteRequestWithError {
 
-			downloader.withDownloadErrorHandling(runnable);
+		private final Throwable error = new Throwable();
 
-			verify(runnable).run();
+		@BeforeEach
+		void init() {
+			doNothing().when(downloader).closeInputStream();
 		}
 
-		@Nested
-		class OnException {
-
-			@Mock
-			private PipedOutputStream outputStream;
-			@Mock
-			private PipedInputStream inputStream;
-
-			private final TechnicalException exception = new TechnicalException("dummy");
-
-			@BeforeEach
-			void init() {
-				setInputStreamField(inputStream);
-				setOutputStreamField(outputStream);
-			}
-
-			@SneakyThrows
-			@Test
-			void shouldCloseOutputStream() {
-				withDownloadErrorHandling();
+		@Test
+		void shouldSetRequestFinished() {
+			assertThat(getRequestFinished()).isFalse();
 
-				verify(outputStream).close();
-			}
+			downloader.completeRequestWithError(error);
 
-			@SneakyThrows
-			@Test
-			void shouldCloseInputStream() {
-				withDownloadErrorHandling();
-
-				verify(inputStream).close();
-			}
+			assertThat(getRequestFinished()).isTrue();
+		}
 
-			@Test
-			void shouldNotifyCallObserver() {
-				withDownloadErrorHandling();
+		@Test
+		void shouldCloseInputStream() {
+			downloader.completeRequestWithError(error);
 
-				verify(callObserverWrapper).onError(argThat(TechnicalException.class::isInstance));
-			}
+			verify(downloader).closeInputStream();
+		}
 
-			private void withDownloadErrorHandling() {
-				downloader.withDownloadErrorHandling(this::dummyMethodThrowingException);
-			}
+		@Test
+		void shouldNotifyCallObserver() {
+			downloader.completeRequestWithError(error);
 
-			private void dummyMethodThrowingException() throws TechnicalException {
-				throw exception;
-			}
+			verify(callObserver).onError(error);
 		}
 	}
 
@@ -527,10 +663,6 @@ class GrpcBinaryFileServerDownloaderTest {
 		ReflectionTestUtils.setField(downloader, "inputStream", inputStream);
 	}
 
-	private void setDownloadFinishedField(boolean downloadFinished) {
-		ReflectionTestUtils.setField(downloader, "downloadFinished", new AtomicBoolean(downloadFinished));
-	}
-
 	private void setRequestFinishedField(boolean requestFinished) {
 		ReflectionTestUtils.setField(downloader, "requestFinished", new AtomicBoolean(requestFinished));
 	}
@@ -539,6 +671,22 @@ class GrpcBinaryFileServerDownloaderTest {
 		return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get();
 	}
 
+	private void setErrorField(BinaryFileDownloadException error) {
+		ReflectionTestUtils.setField(downloader, "error", new AtomicReference<>(error));
+	}
+
+	private BinaryFileDownloadException getError() {
+		return (BinaryFileDownloadException) ReflectionTestUtils.getField(downloader, "error", AtomicReference.class).get();
+	}
+
+	private void setDownloadFinishedField(boolean downloadFinished) {
+		ReflectionTestUtils.setField(downloader, "downloadFinished", new AtomicBoolean(downloadFinished));
+	}
+
+	private boolean getDownloadFinished() {
+		return ReflectionTestUtils.getField(downloader, "downloadFinished", AtomicBoolean.class).get();
+	}
+
 	private static class GrpcResponseDummy {
 	}
 }
\ No newline at end of file
-- 
GitLab


From 844a1a873540338915ee92a5a7c5c41807bf7a95 Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Thu, 30 Jan 2025 10:42:48 +0100
Subject: [PATCH 4/7] OZG-7262 OZG-7627 Remove BinaryFileDownloadException

---
 .../BinaryFileDownloadException.java          | 33 -------------------
 .../GrpcBinaryFileServerDownloader.java       | 18 +++++-----
 .../GrpcBinaryFileServerDownloaderTest.java   | 21 +++++++-----
 3 files changed, 22 insertions(+), 50 deletions(-)
 delete mode 100644 ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java
deleted file mode 100644
index 9a5d462..0000000
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileDownloadException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den
- * Ministerpräsidenten des Landes Schleswig-Holstein
- * Staatskanzlei
- * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
- *
- * Lizenziert unter der EUPL, Version 1.2 oder - sobald
- * diese von der Europäischen Kommission genehmigt wurden -
- * Folgeversionen der EUPL ("Lizenz");
- * Sie dürfen dieses Werk ausschließlich gemäß
- * dieser Lizenz nutzen.
- * Eine Kopie der Lizenz finden Sie hier:
- *
- * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- *
- * Sofern nicht durch anwendbare Rechtsvorschriften
- * gefordert oder in schriftlicher Form vereinbart, wird
- * die unter der Lizenz verbreitete Software "so wie sie
- * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
- * ausdrücklich oder stillschweigend - verbreitet.
- * Die sprachspezifischen Genehmigungen und Beschränkungen
- * unter der Lizenz sind dem Lizenztext zu entnehmen.
- */
-package de.ozgcloud.common.binaryfile;
-
-import de.ozgcloud.common.errorhandling.TechnicalException;
-
-class BinaryFileDownloadException extends TechnicalException {
-
-	public BinaryFileDownloadException(Throwable cause) {
-		super("Error occurred during downloading file content", cause);
-	}
-}
diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index 090a53d..b2d2df3 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -27,6 +27,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
@@ -37,6 +38,7 @@ import org.springframework.core.task.TaskExecutor;
 
 import com.google.protobuf.ByteString;
 
+import de.ozgcloud.common.errorhandling.TechnicalException;
 import io.grpc.stub.CallStreamObserver;
 import lombok.Builder;
 import lombok.extern.log4j.Log4j2;
@@ -55,7 +57,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 	private final AtomicBoolean started = new AtomicBoolean(false);
 	private final AtomicBoolean downloadFinished = new AtomicBoolean(false);
 	private final AtomicBoolean requestFinished = new AtomicBoolean(false);
-	private final AtomicReference<BinaryFileDownloadException> error = new AtomicReference<>();
+	private final AtomicReference<TechnicalException> error = new AtomicReference<>();
 
 	private PipedInputStream inputStream;
 	private PipedOutputStream outputStream;
@@ -92,7 +94,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 		} catch (Exception e) {
 			closeOutputStream();
 			closeInputStream();
-			throw new BinaryFileDownloadException(e);
+			throw new TechnicalException("Error while setting up streams", e);
 		}
 	}
 
@@ -106,7 +108,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 		try {
 			doDownload();
 		} catch (Exception e) {
-			error.set(new BinaryFileDownloadException(e));
+			error.set(new TechnicalException("Error while downloading file contents", e));
 		} finally {
 			closeOutputStream();
 			sendChunks();
@@ -124,7 +126,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 		try {
 			doSendChunks();
 		} catch (Exception e) {
-			completeRequestWithError(e);
+			completeRequestWithError(new TechnicalException("Error while sending chunks", e));
 		}
 	}
 
@@ -134,7 +136,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 		}
 		int bytesRead;
 		while (isReady()) {
-			if (error.get() != null) {
+			if (Objects.nonNull(error.get())) {
 				completeRequestWithError(error.get());
 				break;
 			}
@@ -167,11 +169,11 @@ public class GrpcBinaryFileServerDownloader<T> {
 		callObserver.onCompleted();
 	}
 
-	void completeRequestWithError(Throwable t) {
-		LOG.debug("Complete download request with error", t);
+	void completeRequestWithError(TechnicalException e) {
+		LOG.debug("Complete download request with error", e);
 		requestFinished.set(true);
 		closeInputStream();
-		callObserver.onError(t);
+		callObserver.onError(e);
 	}
 
 	void closeOutputStream() {
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
index 2e45282..81da69e 100644
--- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
+++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
@@ -191,7 +191,7 @@ class GrpcBinaryFileServerDownloaderTest {
 
 			@Test
 			void shouldThrowBinaryFileDownloadException() {
-				assertThatThrownBy(() -> downloader.safelySetupStreams()).isInstanceOf(BinaryFileDownloadException.class).hasCause(exception);
+				assertThatThrownBy(() -> downloader.safelySetupStreams()).isInstanceOf(TechnicalException.class).hasCause(exception);
 			}
 
 			@SneakyThrows
@@ -278,7 +278,7 @@ class GrpcBinaryFileServerDownloaderTest {
 			void shouldSetError() {
 				downloader.startDownload();
 
-				assertThat(getError()).isInstanceOf(BinaryFileDownloadException.class).hasCause(exception);
+				assertThat(getError()).isInstanceOf(TechnicalException.class).hasCause(exception);
 			}
 
 			@SneakyThrows
@@ -354,7 +354,9 @@ class GrpcBinaryFileServerDownloaderTest {
 		@Nested
 		class OnException {
 
-			private final TechnicalException exception = new TechnicalException("error");
+			private final IOException exception = new IOException();
+			@Captor
+			private ArgumentCaptor<TechnicalException> argumentCaptor;
 
 			@SneakyThrows
 			@BeforeEach
@@ -366,7 +368,8 @@ class GrpcBinaryFileServerDownloaderTest {
 			void shouldCompleteRequestWithError() {
 				downloader.sendChunks();
 
-				verify(downloader).completeRequestWithError(exception);
+				verify(downloader).completeRequestWithError(argumentCaptor.capture());
+				assertThat(argumentCaptor.getValue()).isInstanceOf(TechnicalException.class).hasCause(exception);
 			}
 		}
 	}
@@ -421,7 +424,7 @@ class GrpcBinaryFileServerDownloaderTest {
 				@Nested
 				class OnHasError {
 
-					private final BinaryFileDownloadException exception = new BinaryFileDownloadException(new TechnicalException("error"));
+					private final TechnicalException exception = new TechnicalException("error");
 
 					@BeforeEach
 					void init() {
@@ -624,7 +627,7 @@ class GrpcBinaryFileServerDownloaderTest {
 	@Nested
 	class TestCompleteRequestWithError {
 
-		private final Throwable error = new Throwable();
+		private final TechnicalException error = new TechnicalException("error");
 
 		@BeforeEach
 		void init() {
@@ -671,12 +674,12 @@ class GrpcBinaryFileServerDownloaderTest {
 		return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get();
 	}
 
-	private void setErrorField(BinaryFileDownloadException error) {
+	private void setErrorField(TechnicalException error) {
 		ReflectionTestUtils.setField(downloader, "error", new AtomicReference<>(error));
 	}
 
-	private BinaryFileDownloadException getError() {
-		return (BinaryFileDownloadException) ReflectionTestUtils.getField(downloader, "error", AtomicReference.class).get();
+	private TechnicalException getError() {
+		return (TechnicalException) ReflectionTestUtils.getField(downloader, "error", AtomicReference.class).get();
 	}
 
 	private void setDownloadFinishedField(boolean downloadFinished) {
-- 
GitLab


From 0c9266270ee2d3014254983ca63312017c9a532d Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Thu, 30 Jan 2025 15:38:34 +0100
Subject: [PATCH 5/7] OZG-7262 OZG-7627 Don't call sendChunks() after exception

---
 .../GrpcBinaryFileServerDownloader.java       |  31 ++-
 .../GrpcBinaryFileServerDownloaderTest.java   | 239 +++++++-----------
 2 files changed, 103 insertions(+), 167 deletions(-)

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index b2d2df3..8acac69 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -57,7 +57,7 @@ public class GrpcBinaryFileServerDownloader<T> {
 	private final AtomicBoolean started = new AtomicBoolean(false);
 	private final AtomicBoolean downloadFinished = new AtomicBoolean(false);
 	private final AtomicBoolean requestFinished = new AtomicBoolean(false);
-	private final AtomicReference<TechnicalException> error = new AtomicReference<>();
+	private final AtomicReference<TechnicalException> downloadError = new AtomicReference<>();
 
 	private PipedInputStream inputStream;
 	private PipedOutputStream outputStream;
@@ -107,11 +107,12 @@ public class GrpcBinaryFileServerDownloader<T> {
 	void startDownload() {
 		try {
 			doDownload();
+			sendChunks();
 		} catch (Exception e) {
-			error.set(new TechnicalException("Error while downloading file contents", e));
+			LOG.error("Error while downloading file contents", e);
+			downloadError.set(new TechnicalException("Error while downloading file contents", e));
 		} finally {
 			closeOutputStream();
-			sendChunks();
 		}
 	}
 
@@ -136,10 +137,6 @@ public class GrpcBinaryFileServerDownloader<T> {
 		}
 		int bytesRead;
 		while (isReady()) {
-			if (Objects.nonNull(error.get())) {
-				completeRequestWithError(error.get());
-				break;
-			}
 			if ((bytesRead = inputStream.read(buffer)) == -1) {
 				tryCompleteRequest();
 				break;
@@ -154,26 +151,28 @@ public class GrpcBinaryFileServerDownloader<T> {
 	}
 
 	void tryCompleteRequest() {
-		if (shouldCompleteRequest()) {
-			completeRequest();
+		if (Objects.nonNull(downloadError.get())) {
+			throw downloadError.get();
+		} else if (downloadFinished.get()) {
+			completeRequestNormally();
 		}
 	}
 
-	boolean shouldCompleteRequest() {
-		return downloadFinished.get() && requestFinished.compareAndSet(false, true);
+	void completeRequestWithError(TechnicalException e) {
+		LOG.debug("Complete download request with error");
+		finishRequest();
+		throw e;
 	}
 
-	void completeRequest() {
+	void completeRequestNormally() {
 		LOG.debug("Complete download request");
-		closeInputStream();
+		finishRequest();
 		callObserver.onCompleted();
 	}
 
-	void completeRequestWithError(TechnicalException e) {
-		LOG.debug("Complete download request with error", e);
+	private void finishRequest() {
 		requestFinished.set(true);
 		closeInputStream();
-		callObserver.onError(e);
 	}
 
 	void closeOutputStream() {
diff --git a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
index 81da69e..8c3ad5a 100644
--- a/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
+++ b/ozgcloud-common-lib/src/test/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloaderTest.java
@@ -229,7 +229,7 @@ class GrpcBinaryFileServerDownloaderTest {
 
 		@Test
 		void shouldErrorBeInitiallyNull() {
-			assertThat(getError()).isNull();
+			assertThat(getDownloadError()).isNull();
 		}
 
 		@Nested
@@ -256,7 +256,6 @@ class GrpcBinaryFileServerDownloaderTest {
 			}
 
 			@Test
-			@DisplayName("should send chunks here to not wait for callObserver to change its ready status")
 			void shouldSendChunks() {
 				downloader.startDownload();
 
@@ -278,7 +277,7 @@ class GrpcBinaryFileServerDownloaderTest {
 			void shouldSetError() {
 				downloader.startDownload();
 
-				assertThat(getError()).isInstanceOf(TechnicalException.class).hasCause(exception);
+				assertThat(getDownloadError()).isInstanceOf(TechnicalException.class).hasCause(exception);
 			}
 
 			@SneakyThrows
@@ -288,14 +287,6 @@ class GrpcBinaryFileServerDownloaderTest {
 
 				verify(outputStream).close();
 			}
-
-			@Test
-			@DisplayName("should send chunks here to communicate error to callObserver")
-			void shouldSendChunks() {
-				downloader.startDownload();
-
-				verify(downloader).sendChunks();
-			}
 		}
 	}
 
@@ -362,6 +353,7 @@ class GrpcBinaryFileServerDownloaderTest {
 			@BeforeEach
 			void init() {
 				doThrow(exception).when(downloader).doSendChunks();
+				doNothing().when(downloader).completeRequestWithError(any());
 			}
 
 			@Test
@@ -416,87 +408,52 @@ class GrpcBinaryFileServerDownloaderTest {
 			@Nested
 			class OnReady {
 
+				@Mock
+				private PipedInputStream inputStream;
+				@Captor
+				private ArgumentCaptor<ByteString> byteStringCaptor;
+
+				private final int readBytes = 20;
+				private final byte[] buffer = new byte[readBytes];
+				private final GrpcResponseDummy grpcResponseDummy = new GrpcResponseDummy();
+
+				@SneakyThrows
 				@BeforeEach
-				void init() {
+				void mock() {
+					doNothing().when(downloader).tryCompleteRequest();
 					when(callObserver.isReady()).thenReturn(true);
+					when(inputStream.read(any())).thenReturn(readBytes, -1);
+					setInputStreamField(inputStream);
+					new Random().nextBytes(buffer);
+					ReflectionTestUtils.setField(downloader, "buffer", buffer);
 				}
 
-				@Nested
-				class OnHasError {
+				@Test
+				void shouldCallChunkBuilder() {
+					doSendChunks();
 
-					private final TechnicalException exception = new TechnicalException("error");
+					verify(chunkBuilder).apply(byteStringCaptor.capture());
+					assertThat(byteStringCaptor.getValue().toByteArray()).isEqualTo(buffer);
+				}
 
-					@BeforeEach
-					void init() {
-						setErrorField(exception);
-						doNothing().when(downloader).completeRequestWithError(any());
-					}
+				@DisplayName("should send next chunk if callObserver is ready and stream already received data")
+				@Test
+				void shouldCallOnNext() {
+					when(chunkBuilder.apply(any())).thenReturn(grpcResponseDummy);
 
-					@Test
-					void shouldOnlyCallIsReadyOnObserver() {
-						doSendChunks();
+					doSendChunks();
 
-						verify(callObserver).isReady();
-						verifyNoMoreInteractions(callObserver);
-					}
+					verify(callObserver).onNext(grpcResponseDummy);
+				}
 
-					@Test
-					void shouldCompleteRequestWithError() {
-						doSendChunks();
+				@DisplayName("should call complete grpc stream if download has finished and stream has no data left")
+				@Test
+				void shouldTryCompleteRequest() {
+					setDownloadFinishedField(true);
 
-						verify(downloader).completeRequestWithError(exception);
-					}
-				}
+					doSendChunks();
 
-				@Nested
-				class OnHasNoError {
-					@Mock
-					private PipedInputStream inputStream;
-					@Captor
-					private ArgumentCaptor<ByteString> byteStringCaptor;
-
-					private final int readBytes = 20;
-					private final byte[] buffer = new byte[readBytes];
-					private final GrpcResponseDummy grpcResponseDummy = new GrpcResponseDummy();
-
-					@SneakyThrows
-					@BeforeEach
-					void mock() {
-						doNothing().when(downloader).tryCompleteRequest();
-						when(callObserver.isReady()).thenReturn(true);
-						when(inputStream.read(any())).thenReturn(readBytes, -1);
-						setInputStreamField(inputStream);
-						new Random().nextBytes(buffer);
-						ReflectionTestUtils.setField(downloader, "buffer", buffer);
-					}
-
-					@Test
-					void shouldCallChunkBuilder() {
-						doSendChunks();
-
-						verify(chunkBuilder).apply(byteStringCaptor.capture());
-						assertThat(byteStringCaptor.getValue().toByteArray()).isEqualTo(buffer);
-					}
-
-					@DisplayName("should send next chunk if callObserver is ready and stream already received data")
-					@Test
-					void shouldCallOnNext() {
-						when(chunkBuilder.apply(any())).thenReturn(grpcResponseDummy);
-
-						doSendChunks();
-
-						verify(callObserver).onNext(grpcResponseDummy);
-					}
-
-					@DisplayName("should call complete grpc stream if download has finished and stream has no data left")
-					@Test
-					void shouldTryCompleteRequest() {
-						setDownloadFinishedField(true);
-
-						doSendChunks();
-
-						verify(downloader).tryCompleteRequest();
-					}
+					verify(downloader).tryCompleteRequest();
 				}
 			}
 		}
@@ -510,69 +467,48 @@ class GrpcBinaryFileServerDownloaderTest {
 	@Nested
 	class TestTryCompleteRequest {
 
-		@Test
-		void shouldCallShouldCompleteRequest() {
-			downloader.tryCompleteRequest();
-
-			verify(downloader).shouldCompleteRequest();
-		}
-
-		@Test
-		void shouldCallCompleteRequest() {
-			doReturn(true).when(downloader).shouldCompleteRequest();
-
-			downloader.tryCompleteRequest();
-
-			verify(downloader).completeRequest();
-		}
+		@Nested
+		class OnError {
 
-		@Test
-		void shouldNotCallCompleteRequest() {
-			doReturn(false).when(downloader).shouldCompleteRequest();
+			private final TechnicalException exception = new TechnicalException("error");
 
-			downloader.tryCompleteRequest();
+			@BeforeEach
+			void init() {
+				setDownloadErrorField(exception);
+			}
 
-			verify(downloader, never()).completeRequest();
+			@Test
+			void shouldThrowException() {
+				assertThatThrownBy(downloader::tryCompleteRequest).isSameAs(exception);
+			}
 		}
-	}
-
-	@Nested
-	class TestShouldCompleteRequest {
 
 		@Nested
-		class TestWhenDownloadFinished {
+		class OnDownloadFinished {
 
 			@BeforeEach
 			void init() {
 				setDownloadFinishedField(true);
+				doNothing().when(downloader).completeRequestNormally();
 			}
 
 			@Test
-			void shouldReturnTrue() {
-				var result = downloader.shouldCompleteRequest();
+			void shouldNotCompleteRequestWithError() {
+				downloader.tryCompleteRequest();
 
-				assertThat(result).isTrue();
+				verify(downloader, never()).completeRequestWithError(any());
 			}
 
 			@Test
-			void shouldReturnFalseIfRequestFinished() {
-				setRequestFinishedField(true);
-
-				var result = downloader.shouldCompleteRequest();
+			void shouldCompleteRequestNormally() {
+				downloader.tryCompleteRequest();
 
-				assertThat(result).isFalse();
-			}
-
-			@Test
-			void shouldUpdateRequestFinished() {
-				downloader.shouldCompleteRequest();
-
-				assertThat(getRequestFinished()).isTrue();
+				verify(downloader).completeRequestNormally();
 			}
 		}
 
 		@Nested
-		class TestWhenDownloadRunning {
+		class OnDownloadNotFinished {
 
 			@BeforeEach
 			void init() {
@@ -580,45 +516,48 @@ class GrpcBinaryFileServerDownloaderTest {
 			}
 
 			@Test
-			void shouldReturnFalse() {
-				var result = downloader.shouldCompleteRequest();
+			void shouldNotCompleteRequestNormally() {
+				downloader.tryCompleteRequest();
 
-				assertThat(result).isFalse();
+				verify(downloader, never()).completeRequestNormally();
 			}
 
 			@Test
-			void shouldNotUpdateRequestFinished() {
-				downloader.shouldCompleteRequest();
+			void shouldNotCompleteRequestWithError() {
+				downloader.tryCompleteRequest();
 
-				assertThat(getRequestFinished()).isFalse();
+				verify(downloader, never()).completeRequestWithError(any());
 			}
 		}
 	}
 
 	@Nested
-	class TestCompleteRequest {
-
-		@Mock
-		private PipedInputStream inputStream;
+	class TestCompleteRequestNormally {
 
 		@BeforeEach
-		void mock() {
-			setRequestFinishedField(false);
-			setDownloadFinishedField(true);
-			setInputStreamField(inputStream);
+		void init() {
+			doNothing().when(downloader).closeInputStream();
 		}
 
-		@SneakyThrows
 		@Test
-		void shouldCallCloseInputStream() {
-			downloader.completeRequest();
+		void shouldSetRequestFinished() {
+			assertThat(getRequestFinished()).isFalse();
+
+			downloader.completeRequestNormally();
+
+			assertThat(getRequestFinished()).isTrue();
+		}
+
+		@Test
+		void shouldCloseInputStream() {
+			downloader.completeRequestNormally();
 
 			verify(downloader).closeInputStream();
 		}
 
 		@Test
-		void shouldCallOnCompleted() {
-			downloader.completeRequest();
+		void shouldNotifyObserver() {
+			downloader.completeRequestNormally();
 
 			verify(callObserver).onCompleted();
 		}
@@ -638,23 +577,21 @@ class GrpcBinaryFileServerDownloaderTest {
 		void shouldSetRequestFinished() {
 			assertThat(getRequestFinished()).isFalse();
 
-			downloader.completeRequestWithError(error);
+			catchException(() -> downloader.completeRequestWithError(error));
 
 			assertThat(getRequestFinished()).isTrue();
 		}
 
 		@Test
 		void shouldCloseInputStream() {
-			downloader.completeRequestWithError(error);
+			catchException(() -> downloader.completeRequestWithError(error));
 
 			verify(downloader).closeInputStream();
 		}
 
 		@Test
-		void shouldNotifyCallObserver() {
-			downloader.completeRequestWithError(error);
-
-			verify(callObserver).onError(error);
+		void shouldThrowException() {
+			assertThatThrownBy(() -> downloader.completeRequestWithError(error)).isSameAs(error);
 		}
 	}
 
@@ -674,12 +611,12 @@ class GrpcBinaryFileServerDownloaderTest {
 		return ReflectionTestUtils.getField(downloader, "requestFinished", AtomicBoolean.class).get();
 	}
 
-	private void setErrorField(TechnicalException error) {
-		ReflectionTestUtils.setField(downloader, "error", new AtomicReference<>(error));
+	private void setDownloadErrorField(TechnicalException error) {
+		ReflectionTestUtils.setField(downloader, "downloadError", new AtomicReference<>(error));
 	}
 
-	private TechnicalException getError() {
-		return (TechnicalException) ReflectionTestUtils.getField(downloader, "error", AtomicReference.class).get();
+	private TechnicalException getDownloadError() {
+		return (TechnicalException) ReflectionTestUtils.getField(downloader, "downloadError", AtomicReference.class).get();
 	}
 
 	private void setDownloadFinishedField(boolean downloadFinished) {
-- 
GitLab


From 45401b678402dc2653153b08f1561d4ca35af051 Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Thu, 30 Jan 2025 15:48:40 +0100
Subject: [PATCH 6/7] OZG-7262 OZG-7627 Log TechnicalException

---
 .../common/binaryfile/GrpcBinaryFileServerDownloader.java       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index 8acac69..edfd2eb 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -109,8 +109,8 @@ public class GrpcBinaryFileServerDownloader<T> {
 			doDownload();
 			sendChunks();
 		} catch (Exception e) {
-			LOG.error("Error while downloading file contents", e);
 			downloadError.set(new TechnicalException("Error while downloading file contents", e));
+			LOG.error(downloadError.get().getMessage(), downloadError.get());
 		} finally {
 			closeOutputStream();
 		}
-- 
GitLab


From 884dcc0b6f05d6de3086046f6f7ebcd5d16b4abd Mon Sep 17 00:00:00 2001
From: Krzysztof Witukiewicz <krzysztof.witukiewicz@mgm-tp.com>
Date: Fri, 31 Jan 2025 08:56:40 +0100
Subject: [PATCH 7/7] OZG-7262 OZG-7627 Remove log

---
 .../common/binaryfile/GrpcBinaryFileServerDownloader.java        | 1 -
 1 file changed, 1 deletion(-)

diff --git a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
index edfd2eb..6083de9 100644
--- a/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
+++ b/ozgcloud-common-lib/src/main/java/de/ozgcloud/common/binaryfile/GrpcBinaryFileServerDownloader.java
@@ -110,7 +110,6 @@ public class GrpcBinaryFileServerDownloader<T> {
 			sendChunks();
 		} catch (Exception e) {
 			downloadError.set(new TechnicalException("Error while downloading file contents", e));
-			LOG.error(downloadError.get().getMessage(), downloadError.get());
 		} finally {
 			closeOutputStream();
 		}
-- 
GitLab