diff --git a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java index 1b21f06096..cf4c1178cf 100644 --- a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java +++ b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.net.URI; +import java.time.Duration; +import java.time.Instant; import java.util.function.Function; import javax.annotation.Nonnull; @@ -104,11 +106,15 @@ private ClassicHttpResponse requestResource( @Nonnull final Function v.listenOnRequest(httpRequest)); + final Instant start = Instant.now(); try { - return httpClient.executeOpen(null, httpRequest, null); + final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, null); + odataRequest.getListeners().forEach(v -> v.listenOnResponse(response)); + return response; } catch( final ClientProtocolException e ) { log.debug("Connection could not be established.", e); + odataRequest.getListeners().forEach(v -> v.listenOnRequestError(e)); throw new ODataConnectionException( this.odataRequest, httpRequest, @@ -117,6 +123,7 @@ private ClassicHttpResponse requestResource( @Nonnull final Function v.listenOnRequestError(e)); throw new ODataConnectionException(this.odataRequest, httpRequest, """ Time out occurred because of a probable connection leak. Please execute your request \ with try-with-resources to ensure resources are properly closed. \ @@ -127,12 +134,18 @@ private ClassicHttpResponse requestResource( @Nonnull final Function v.listenOnRequestError(e)); throw new ODataConnectionException(this.odataRequest, httpRequest, "Connection was aborted.", e); } catch( final Exception e ) { log.debug("Connection failed.", e); + odataRequest.getListeners().forEach(v -> v.listenOnRequestError(e)); throw new ODataConnectionException(this.odataRequest, httpRequest, "Connection failed.", e); } + finally { + final Duration duration = Duration.between(start, Instant.now()); + odataRequest.getListeners().forEach(v -> v.listenOnExecutionFinished(duration)); + } } /** diff --git a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java index 7878f67be0..6e33e4df1f 100644 --- a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java +++ b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java @@ -65,7 +65,6 @@ public abstract class ODataRequestGeneric implements ODataRequestExecutable /** * List of listeners to observe and react on OData actions. */ - @Getter( AccessLevel.PROTECTED ) private final List listeners = new ArrayList<>(); /** @@ -96,6 +95,18 @@ public abstract class ODataRequestGeneric implements ODataRequestExecutable headers.putIfAbsent(HttpHeaders.ACCEPT, Lists.newArrayList(DEFAULT_FORMAT.getHttpAccept())); } + /** + * Get the list of listeners to observe and react on OData actions. + * + * @return The list of listeners. + * @since 5.35.0 + */ + @Nonnull + public List getListeners() + { + return listeners; + } + /** * Get the static request URI of the OData resource. * diff --git a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java index e70b3396fc..332f360e1e 100644 --- a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java +++ b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java @@ -1,8 +1,11 @@ package com.sap.cloud.sdk.datamodel.odata.client.request; +import java.time.Duration; + import javax.annotation.Nonnull; import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.core5.http.ClassicHttpResponse; /** * Consumer class for the Listener Pattern to monitor and react on OData actions. @@ -19,6 +22,28 @@ public interface ODataRequestListener */ void listenOnRequest( @Nonnull final HttpUriRequestBase request ); + /** + * Handler to react after execution of an HTTP request, when the response is received. + * + * @param response + * The HTTP response. + * @since 5.35.0 + */ + default void listenOnResponse( @Nonnull final ClassicHttpResponse response ) + { + } + + /** + * Handler to react after the request execution has finished (either successfully or with an error). + * + * @param duration + * The duration of the request execution. + * @since 5.35.0 + */ + default void listenOnExecutionFinished( @Nonnull final Duration duration ) + { + } + /** * Handler to react on an error during request generation. * diff --git a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java index 8c223207b6..51b9c2efed 100644 --- a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java +++ b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java @@ -43,7 +43,12 @@ interface ODataRequestResultFactory .onEmpty(() -> log.debug("HTTP response entity is empty: {}", status)) .map(entity -> Try.run(() -> copy.setEntity(new BufferedHttpEntity(entity)))) .peek(b -> b.onSuccess(v -> log.debug("Successfully buffered the HTTP response entity."))) - .peek(b -> b.onFailure(e -> log.warn("Failed to buffer HTTP response entity: {}", status, e))); + .peek(b -> b.onFailure(t -> { + log.warn("Failed to buffer HTTP response entity: {}", status, t); + if( t instanceof Exception ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError((Exception) t)); + } + })); Try.run(httpResponse::close).onFailure(e -> log.warn("Failed to close HTTP response: {}", status, e)); diff --git a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java index c7ee5bfd03..6266eb5843 100644 --- a/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java +++ b/datamodel/odata-client-apache-httpclient5/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java @@ -165,21 +165,27 @@ public void streamElements( @Nonnull final Consumer handler ) { final GsonResultElementFactory resultElementFactory = getResultElementFactory(); - final Integer numConsumedElements = HttpEntityReader.stream(this, reader -> { - deserializer.positionReaderToResultSet(reader); - - int count = 0; - while( reader.hasNext() && reader.peek() == JsonToken.BEGIN_OBJECT ) { - final JsonElement jsonElement = JsonParser.parseReader(reader); - final ResultElement resultElement = resultElementFactory.create(jsonElement); - handler.accept(resultElement); - count++; - } - reader.close(); - return count; - }); - - log.debug("Iterated {} elements.", numConsumedElements); + try { + final Integer numConsumedElements = HttpEntityReader.stream(this, reader -> { + deserializer.positionReaderToResultSet(reader); + + int count = 0; + while( reader.hasNext() && reader.peek() == JsonToken.BEGIN_OBJECT ) { + final JsonElement jsonElement = JsonParser.parseReader(reader); + final ResultElement resultElement = resultElementFactory.create(jsonElement); + handler.accept(resultElement); + count++; + } + reader.close(); + return count; + }); + + log.debug("Iterated {} elements.", numConsumedElements); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } } private GsonResultElementFactory getResultElementFactory() @@ -204,22 +210,32 @@ private ResultPrimitive loadPrimitiveFromResponse( @Nonnull final Function jsonElementExtractor ) { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultPrimitive result = HttpEntityReader.read(this, element -> { - final Option single = - deserializer - .getElementToResultPrimitiveSingle(element) - .map(jsonElementExtractor) - .map(elementFactory::create) - .map(ResultElement::getAsPrimitive); - return single.getOrNull(); - }); + final ResultPrimitive result; + try { + result = HttpEntityReader.read(this, element -> { + final Option single = + deserializer + .getElementToResultPrimitiveSingle(element) + .map(jsonElementExtractor) + .map(elementFactory::create) + .map(ResultElement::getAsPrimitive); + return single.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as a primitive value.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -229,21 +245,31 @@ private ResultCollection loadPrimitiveCollectionFromResponse() { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultCollection result = HttpEntityReader.read(this, element -> { - final Option set = - deserializer - .getElementToResultPrimitiveSet(element) - .map(elementFactory::create) - .map(ResultElement::getAsCollection); - return set.getOrNull(); - }); + final ResultCollection result; + try { + result = HttpEntityReader.read(this, element -> { + final Option set = + deserializer + .getElementToResultPrimitiveSet(element) + .map(elementFactory::create) + .map(ResultElement::getAsCollection); + return set.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as set of primitive values.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -252,22 +278,32 @@ private ResultCollection loadPrimitiveCollectionFromResponse() private ResultObject loadEntryFromResponse( @Nonnull final Function jsonElementExtractor ) { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultObject result = HttpEntityReader.read(this, element -> { - final Option single = - deserializer - .getElementToResultSingle(element) - .map(jsonElementExtractor) - .map(elementFactory::create) - .map(ResultElement::getAsObject); - return single.getOrNull(); - }); + final ResultObject result; + try { + result = HttpEntityReader.read(this, element -> { + final Option single = + deserializer + .getElementToResultSingle(element) + .map(jsonElementExtractor) + .map(elementFactory::create) + .map(ResultElement::getAsObject); + return single.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as a single entity.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -277,21 +313,31 @@ private ResultCollection loadEntryCollectionFromResponse() { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultCollection result = HttpEntityReader.read(this, element -> { - final Option set = - deserializer - .getElementToResultSet(element) - .map(elementFactory::create) - .map(ResultElement::getAsCollection); - return set.getOrNull(); - }); + final ResultCollection result; + try { + result = HttpEntityReader.read(this, element -> { + final Option set = + deserializer + .getElementToResultSet(element) + .map(elementFactory::create) + .map(ResultElement::getAsCollection); + return set.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as set of entities.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -690,11 +736,14 @@ public boolean hasPayload() private void assertNonEmptyPayload() { if( !hasPayload() ) { - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - protocol + " response did not contain any payload.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + protocol + " response did not contain any payload.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } } diff --git a/datamodel/odata-client-apache-httpclient5/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListenerTest.java b/datamodel/odata-client-apache-httpclient5/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListenerTest.java new file mode 100644 index 0000000000..6bb8af5bcd --- /dev/null +++ b/datamodel/odata-client-apache-httpclient5/src/test/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListenerTest.java @@ -0,0 +1,101 @@ +package com.sap.cloud.sdk.datamodel.odata.client.request; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Duration; + +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.junit.jupiter.api.Test; + +import com.sap.cloud.sdk.datamodel.odata.client.ODataProtocol; +import com.sap.cloud.sdk.datamodel.odata.client.expression.ODataResourcePath; + +class ODataRequestListenerTest +{ + @Test + void testListenerIsCalled() + throws Exception + { + final ODataRequestListener listener = mock(ODataRequestListener.class); + final HttpClient httpClient = mock(HttpClient.class); + final ClassicHttpResponse httpResponse = mock(ClassicHttpResponse.class); + final HttpEntity entity = new StringEntity("{\"d\":{\"results\":[]}}", ContentType.APPLICATION_JSON); + + when(httpClient.executeOpen(any(), any(), any())).thenReturn(httpResponse); + when(httpResponse.getEntity()).thenReturn(entity); + when(httpResponse.getCode()).thenReturn(200); + + final ODataRequestRead request = + new ODataRequestRead("service", ODataResourcePath.of("entity"), "", ODataProtocol.V2); + request.addListener(listener); + + request.execute(httpClient); + + verify(listener).listenOnRequest(any()); + verify(listener).listenOnResponse(any()); + verify(listener).listenOnExecutionFinished(any(Duration.class)); + } + + @Test + void testListenerIsCalledOnError() + throws Exception + { + final ODataRequestListener listener = mock(ODataRequestListener.class); + final HttpClient httpClient = mock(HttpClient.class); + + when(httpClient.executeOpen(any(), any(), any())).thenThrow(new RuntimeException("error")); + + final ODataRequestRead request = + new ODataRequestRead("service", ODataResourcePath.of("entity"), "", ODataProtocol.V2); + request.addListener(listener); + + try { + request.execute(httpClient); + } + catch( final Exception e ) { + // expected + } + + verify(listener).listenOnRequest(any()); + verify(listener).listenOnRequestError(any()); + verify(listener).listenOnExecutionFinished(any(Duration.class)); + } + + @Test + void testListenerIsCalledOnParsingError() + throws Exception + { + final ODataRequestListener listener = mock(ODataRequestListener.class); + final HttpClient httpClient = mock(HttpClient.class); + final ClassicHttpResponse httpResponse = mock(ClassicHttpResponse.class); + // Valid JSON but invalid OData structure to trigger mapping error + final HttpEntity entity = new StringEntity("{\"d\": \"invalid\"}", ContentType.APPLICATION_JSON); + + when(httpClient.executeOpen(any(), any(), any())).thenReturn(httpResponse); + when(httpResponse.getEntity()).thenReturn(entity); + when(httpResponse.getCode()).thenReturn(200); + + final ODataRequestRead request = + new ODataRequestRead("service", ODataResourcePath.of("entity"), "", ODataProtocol.V2); + request.addListener(listener); + + try { + request.execute(httpClient).asList(Object.class); + } + catch( final Exception e ) { + // expected + } + + verify(listener).listenOnRequest(any()); + verify(listener).listenOnResponse(any()); + verify(listener).listenOnParsingError(any()); + verify(listener).listenOnExecutionFinished(any(Duration.class)); + } +} diff --git a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java index 6fa68d4b6f..eeb61f7158 100644 --- a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java +++ b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataHttpRequest.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.net.URI; +import java.time.Duration; +import java.time.Instant; import java.util.function.Function; import javax.annotation.Nonnull; @@ -105,11 +107,15 @@ private HttpResponse requestResource( @Nonnull final Function v.listenOnRequest(httpRequest)); + final Instant start = Instant.now(); try { - return httpClient.execute(httpRequest); + final HttpResponse response = httpClient.execute(httpRequest); + odataRequest.getListeners().forEach(v -> v.listenOnResponse(response)); + return response; } catch( final ClientProtocolException e ) { log.debug("Connection could not be established.", e); + odataRequest.getListeners().forEach(v -> v.listenOnRequestError(e)); throw new ODataConnectionException( this.odataRequest, httpRequest, @@ -118,6 +124,7 @@ private HttpResponse requestResource( @Nonnull final Function v.listenOnRequestError(e)); throw new ODataConnectionException( this.odataRequest, httpRequest, @@ -130,12 +137,18 @@ private HttpResponse requestResource( @Nonnull final Function v.listenOnRequestError(e)); throw new ODataConnectionException(this.odataRequest, httpRequest, "Connection was aborted.", e); } catch( final Exception e ) { log.debug("Connection failed.", e); + odataRequest.getListeners().forEach(v -> v.listenOnRequestError(e)); throw new ODataConnectionException(this.odataRequest, httpRequest, "Connection failed.", e); } + finally { + final Duration duration = Duration.between(start, Instant.now()); + odataRequest.getListeners().forEach(v -> v.listenOnExecutionFinished(duration)); + } } /** diff --git a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java index e410acea07..1b381bf5ee 100644 --- a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java +++ b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestGeneric.java @@ -68,7 +68,6 @@ public abstract class ODataRequestGeneric implements ODataRequestExecutable /** * List of listeners to observe and react on OData actions. */ - @Getter( AccessLevel.PROTECTED ) private final List listeners = new ArrayList<>(); /** @@ -106,6 +105,18 @@ public abstract class ODataRequestGeneric implements ODataRequestExecutable headers.putIfAbsent(HttpHeaders.ACCEPT, Lists.newArrayList(DEFAULT_FORMAT.getHttpAccept())); } + /** + * Get the list of listeners to observe and react on OData actions. + * + * @return The list of listeners. + * @since 5.35.0 + */ + @Nonnull + public List getListeners() + { + return listeners; + } + /** * Get the static request URI of the OData resource. * diff --git a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java index 61e694a8bf..c81ea9faf2 100644 --- a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java +++ b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestListener.java @@ -1,7 +1,10 @@ package com.sap.cloud.sdk.datamodel.odata.client.request; +import java.time.Duration; + import javax.annotation.Nonnull; +import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; /** @@ -17,6 +20,28 @@ public interface ODataRequestListener */ void listenOnRequest( @Nonnull final HttpRequestBase request ); + /** + * Handler to react after execution of an HTTP request, when the response is received. + * + * @param response + * The HTTP response. + * @since 5.35.0 + */ + default void listenOnResponse( @Nonnull final HttpResponse response ) + { + } + + /** + * Handler to react after the request execution has finished (either successfully or with an error). + * + * @param duration + * The duration of the request execution. + * @since 5.35.0 + */ + default void listenOnExecutionFinished( @Nonnull final Duration duration ) + { + } + /** * Handler to react on an error during request generation. * diff --git a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java index 7e4c07a125..c26bda69e5 100644 --- a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java +++ b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultFactory.java @@ -41,7 +41,12 @@ interface ODataRequestResultFactory .onEmpty(() -> log.debug("HTTP response entity is empty: {}", status)) .map(entity -> Try.run(() -> copy.setEntity(new BufferedHttpEntity(entity)))) .peek(b -> b.onSuccess(v -> log.debug("Successfully buffered the HTTP response entity."))) - .peek(b -> b.onFailure(e -> log.warn("Failed to buffer HTTP response entity: {}", status, e))); + .peek(b -> b.onFailure(t -> { + log.warn("Failed to buffer HTTP response entity: {}", status, t); + if( t instanceof Exception ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError((Exception) t)); + } + })); return new ODataRequestResultGeneric(oDataRequest, copy, httpClient); }; diff --git a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java index 5a36518885..3f22f917b7 100644 --- a/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java +++ b/datamodel/odata-client/src/main/java/com/sap/cloud/sdk/datamodel/odata/client/request/ODataRequestResultGeneric.java @@ -163,21 +163,27 @@ public void streamElements( @Nonnull final Consumer handler ) { final GsonResultElementFactory resultElementFactory = getResultElementFactory(); - final Integer numConsumedElements = HttpEntityReader.stream(this, reader -> { - deserializer.positionReaderToResultSet(reader); - - int count = 0; - while( reader.hasNext() && reader.peek() == JsonToken.BEGIN_OBJECT ) { - final JsonElement jsonElement = JsonParser.parseReader(reader); - final ResultElement resultElement = resultElementFactory.create(jsonElement); - handler.accept(resultElement); - count++; - } - reader.close(); - return count; - }); - - log.debug("Iterated {} elements.", numConsumedElements); + try { + final Integer numConsumedElements = HttpEntityReader.stream(this, reader -> { + deserializer.positionReaderToResultSet(reader); + + int count = 0; + while( reader.hasNext() && reader.peek() == JsonToken.BEGIN_OBJECT ) { + final JsonElement jsonElement = JsonParser.parseReader(reader); + final ResultElement resultElement = resultElementFactory.create(jsonElement); + handler.accept(resultElement); + count++; + } + reader.close(); + return count; + }); + + log.debug("Iterated {} elements.", numConsumedElements); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } } private GsonResultElementFactory getResultElementFactory() @@ -202,22 +208,32 @@ private ResultPrimitive loadPrimitiveFromResponse( @Nonnull final Function jsonElementExtractor ) { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultPrimitive result = HttpEntityReader.read(this, element -> { - final Option single = - deserializer - .getElementToResultPrimitiveSingle(element) - .map(jsonElementExtractor) - .map(elementFactory::create) - .map(ResultElement::getAsPrimitive); - return single.getOrNull(); - }); + final ResultPrimitive result; + try { + result = HttpEntityReader.read(this, element -> { + final Option single = + deserializer + .getElementToResultPrimitiveSingle(element) + .map(jsonElementExtractor) + .map(elementFactory::create) + .map(ResultElement::getAsPrimitive); + return single.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as a primitive value.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -227,21 +243,31 @@ private ResultCollection loadPrimitiveCollectionFromResponse() { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultCollection result = HttpEntityReader.read(this, element -> { - final Option set = - deserializer - .getElementToResultPrimitiveSet(element) - .map(elementFactory::create) - .map(ResultElement::getAsCollection); - return set.getOrNull(); - }); + final ResultCollection result; + try { + result = HttpEntityReader.read(this, element -> { + final Option set = + deserializer + .getElementToResultPrimitiveSet(element) + .map(elementFactory::create) + .map(ResultElement::getAsCollection); + return set.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as set of primitive values.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -250,22 +276,32 @@ private ResultCollection loadPrimitiveCollectionFromResponse() private ResultObject loadEntryFromResponse( @Nonnull final Function jsonElementExtractor ) { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultObject result = HttpEntityReader.read(this, element -> { - final Option single = - deserializer - .getElementToResultSingle(element) - .map(jsonElementExtractor) - .map(elementFactory::create) - .map(ResultElement::getAsObject); - return single.getOrNull(); - }); + final ResultObject result; + try { + result = HttpEntityReader.read(this, element -> { + final Option single = + deserializer + .getElementToResultSingle(element) + .map(jsonElementExtractor) + .map(elementFactory::create) + .map(ResultElement::getAsObject); + return single.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as a single entity.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -275,21 +311,31 @@ private ResultCollection loadEntryCollectionFromResponse() { final GsonResultElementFactory elementFactory = getResultElementFactory(); - final ResultCollection result = HttpEntityReader.read(this, element -> { - final Option set = - deserializer - .getElementToResultSet(element) - .map(elementFactory::create) - .map(ResultElement::getAsCollection); - return set.getOrNull(); - }); + final ResultCollection result; + try { + result = HttpEntityReader.read(this, element -> { + final Option set = + deserializer + .getElementToResultSet(element) + .map(elementFactory::create) + .map(ResultElement::getAsCollection); + return set.getOrNull(); + }); + } + catch( final Exception e ) { + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; + } if( result == null ) { log.debug("{} response cannot be read as set of entities.", protocol); - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - "Unable to read " + protocol + " response.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + "Unable to read " + protocol + " response.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } return result; } @@ -687,11 +733,14 @@ public boolean hasPayload() private void assertNonEmptyPayload() { if( !hasPayload() ) { - throw new ODataDeserializationException( - getODataRequest(), - getHttpResponse(), - protocol + " response did not contain any payload.", - null); + final ODataDeserializationException e = + new ODataDeserializationException( + getODataRequest(), + getHttpResponse(), + protocol + " response did not contain any payload.", + null); + oDataRequest.getListeners().forEach(l -> l.listenOnParsingError(e)); + throw e; } } diff --git a/testutil/src/main/java/com/sap/cloud/sdk/testutil/RequestHeaderContext.java b/testutil/src/main/java/com/sap/cloud/sdk/testutil/RequestHeaderContext.java new file mode 100644 index 0000000000..a971c78e7d --- /dev/null +++ b/testutil/src/main/java/com/sap/cloud/sdk/testutil/RequestHeaderContext.java @@ -0,0 +1,52 @@ +package com.sap.cloud.sdk.testutil; + +import java.util.Map; + +import javax.annotation.Nonnull; + +import com.sap.cloud.sdk.cloudplatform.requestheader.DefaultRequestHeaderContainer; +import com.sap.cloud.sdk.cloudplatform.requestheader.RequestHeaderContainer; +import com.sap.cloud.sdk.cloudplatform.requestheader.RequestHeaderThreadContextListener; + +/** + * API for setting and clearing the request headers for the current thread. + * + * @since 5.35.0 + */ +public interface RequestHeaderContext extends TestContextApi +{ + /** + * Set the given headers for the current thread. + * + * @param headers + * the headers to use + * @return the header container + */ + @Nonnull + default RequestHeaderContainer setRequestHeaders( @Nonnull final Map headers ) + { + return setRequestHeaders(DefaultRequestHeaderContainer.fromSingleValueMap(headers)); + } + + /** + * Set the given headers for the current thread. + * + * @param headers + * the headers to use + * @return the header container + */ + @Nonnull + default RequestHeaderContainer setRequestHeaders( @Nonnull final RequestHeaderContainer headers ) + { + setProperty(RequestHeaderThreadContextListener.PROPERTY_REQUEST_HEADERS, headers); + return headers; + } + + /** + * Clear the request headers for the current thread. + */ + default void clearRequestHeaders() + { + setProperty(RequestHeaderThreadContextListener.PROPERTY_REQUEST_HEADERS, null); + } +} diff --git a/testutil/src/main/java/com/sap/cloud/sdk/testutil/TestContext.java b/testutil/src/main/java/com/sap/cloud/sdk/testutil/TestContext.java index 3c0c5ee68d..c43104006e 100644 --- a/testutil/src/main/java/com/sap/cloud/sdk/testutil/TestContext.java +++ b/testutil/src/main/java/com/sap/cloud/sdk/testutil/TestContext.java @@ -36,7 +36,8 @@ public final class TestContext InvocationInterceptor, TenantContext, PrincipalContext, - AuthTokenContext + AuthTokenContext, + RequestHeaderContext { private final ThreadContext context = new DefaultThreadContext(); diff --git a/testutil/src/test/java/com/sap/cloud/sdk/testutil/TestContextTest.java b/testutil/src/test/java/com/sap/cloud/sdk/testutil/TestContextTest.java index e6a12ddab2..0bd2d63a09 100644 --- a/testutil/src/test/java/com/sap/cloud/sdk/testutil/TestContextTest.java +++ b/testutil/src/test/java/com/sap/cloud/sdk/testutil/TestContextTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import com.sap.cloud.sdk.cloudplatform.requestheader.RequestHeaderAccessor; +import com.sap.cloud.sdk.cloudplatform.requestheader.RequestHeaderContainer; import com.sap.cloud.sdk.cloudplatform.security.AuthToken; import com.sap.cloud.sdk.cloudplatform.security.AuthTokenAccessor; import com.sap.cloud.sdk.cloudplatform.security.principal.Principal; @@ -49,6 +51,21 @@ void testEmptyThreadContext() assertThat(TenantAccessor.tryGetCurrentTenant()).isEmpty(); assertThat(PrincipalAccessor.tryGetCurrentPrincipal()).isEmpty(); assertThat(AuthTokenAccessor.tryGetCurrentToken()).isEmpty(); + assertThat(RequestHeaderAccessor.tryGetHeaderContainer()).isEmpty(); + } + + @Test + @DisplayName( "Request headers should be modifiable" ) + void testRequestHeadersCanBeModified() + { + assertThat(RequestHeaderAccessor.tryGetHeaderContainer()).isEmpty(); + final RequestHeaderContainer headers = + sut.setRequestHeaders(java.util.Collections.singletonMap("foo", "bar")); + assertThat(RequestHeaderAccessor.tryGetHeaderContainer()).contains(headers); + assertThat(RequestHeaderAccessor.getHeaderContainer().getHeaderValues("foo")).containsExactly("bar"); + + sut.clearRequestHeaders(); + assertThat(RequestHeaderAccessor.tryGetHeaderContainer()).isEmpty(); } @Test