1- package com .sap .ai .sdk .orchestration ;
1+ package com .sap .ai .sdk .core . commons ;
22
3- import static com .sap .ai .sdk .orchestration . OrchestrationClient . JACKSON ;
3+ import static com .sap .ai .sdk .core . JacksonConfiguration . getDefaultObjectMapper ;
44
55import com .fasterxml .jackson .core .JsonProcessingException ;
6- import com .sap . ai . sdk . orchestration . model . ErrorResponse ;
6+ import com .fasterxml . jackson . databind . ObjectMapper ;
77import io .vavr .control .Try ;
88import java .io .IOException ;
99import java .nio .charset .StandardCharsets ;
10+ import java .util .function .BiFunction ;
1011import javax .annotation .Nonnull ;
1112import lombok .RequiredArgsConstructor ;
1213import lombok .extern .slf4j .Slf4j ;
1819import org .apache .hc .core5 .http .io .HttpClientResponseHandler ;
1920import org .apache .hc .core5 .http .io .entity .EntityUtils ;
2021
22+ /**
23+ * Parse incoming JSON responses and handles any errors
24+ *
25+ * @param <T> The type of the response
26+ * @param <E> The type of the exception to throw
27+ */
2128@ Slf4j
2229@ RequiredArgsConstructor
23- class OrchestrationResponseHandler <T > implements HttpClientResponseHandler <T > {
30+ public class ClientResponseHandler <T , E extends ClientException >
31+ implements HttpClientResponseHandler <T > {
2432 @ Nonnull private final Class <T > responseType ;
33+ @ Nonnull private final Class <? extends ClientError > errorType ;
34+ @ Nonnull private final BiFunction <String , Throwable , E > exceptionType ;
2535
36+ /** The parses for JSON responses, will be private once we can remove mixins */
37+ @ Nonnull public ObjectMapper JACKSON = getDefaultObjectMapper ();
38+
39+ /**
40+ * Processes a {@link ClassicHttpResponse} and returns some value corresponding to that response.
41+ *
42+ * @param response The response to process
43+ * @return A model class instantiated from the response
44+ * @throws E in case of a problem or the connection was aborted
45+ */
46+ @ Nonnull
47+ @ Override
48+ public T handleResponse (@ Nonnull final ClassicHttpResponse response ) throws E {
49+ if (response .getCode () >= 300 ) {
50+ buildExceptionAndThrow (response );
51+ }
52+ return parseResponse (response );
53+ }
54+
55+ // The InputStream of the HTTP entity is closed by EntityUtils.toString
56+ @ SuppressWarnings ("PMD.CloseResource" )
2657 @ Nonnull
27- private static String getContent (@ Nonnull final HttpEntity entity ) {
58+ private T parseResponse (@ Nonnull final ClassicHttpResponse response ) throws E {
59+ final HttpEntity responseEntity = response .getEntity ();
60+ if (responseEntity == null ) {
61+ throw exceptionType .apply ("Response was empty." , null );
62+ }
63+ val content = getContent (responseEntity );
64+ log .debug ("Parsing response from JSON response: {}" , content );
65+ try {
66+ return JACKSON .readValue (content , responseType );
67+ } catch (final JsonProcessingException e ) {
68+ log .error ("Failed to parse the following response: {}" , content );
69+ throw exceptionType .apply ("Failed to parse response" , e );
70+ }
71+ }
72+
73+ @ Nonnull
74+ private String getContent (@ Nonnull final HttpEntity entity ) {
2875 try {
2976 return EntityUtils .toString (entity , StandardCharsets .UTF_8 );
3077 } catch (IOException | ParseException e ) {
31- throw new OrchestrationClientException ("Failed to read response content." , e );
78+ throw exceptionType . apply ("Failed to read response content." , e );
3279 }
3380 }
3481
35- // The InputStream of the HTTP entity is closed by EntityUtils.toString
82+ /**
83+ * Parse the error response and throw an exception.
84+ *
85+ * @param response The response to process
86+ */
3687 @ SuppressWarnings ("PMD.CloseResource" )
37- static void buildExceptionAndThrow (@ Nonnull final ClassicHttpResponse response )
38- throws OrchestrationClientException {
88+ public void buildExceptionAndThrow (@ Nonnull final ClassicHttpResponse response ) throws E {
3989 val exception =
40- new OrchestrationClientException (
41- "Request to orchestration service failed with status %s %s"
42- .formatted (response .getCode (), response .getReasonPhrase ()));
90+ exceptionType .apply (
91+ "Request failed with status %s %s"
92+ .formatted (response .getCode (), response .getReasonPhrase ()),
93+ null );
4394 val entity = response .getEntity ();
4495 if (entity == null ) {
4596 throw exception ;
@@ -54,9 +105,7 @@ static void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response)
54105 throw exception ;
55106 }
56107
57- log .error (
58- "The orchestration service responded with an HTTP error and the following content: {}" ,
59- content );
108+ log .error ("The service responded with an HTTP error and the following content: {}" , content );
60109 val contentType = ContentType .parse (entity .getContentType ());
61110 if (!ContentType .APPLICATION_JSON .isSameMimeType (contentType )) {
62111 throw exception ;
@@ -68,59 +117,20 @@ static void buildExceptionAndThrow(@Nonnull final ClassicHttpResponse response)
68117 /**
69118 * Parse the error response and throw an exception.
70119 *
71- * @param errorResponse the error response, most likely a JSON of {@link ErrorResponse} .
120+ * @param errorResponse the error response, most likely a unique JSON class .
72121 * @param baseException a base exception to add the error message to.
73122 */
74- static void parseErrorAndThrow (
75- @ Nonnull final String errorResponse ,
76- @ Nonnull final OrchestrationClientException baseException )
77- throws OrchestrationClientException {
78- val maybeError = Try .of (() -> JACKSON .readValue (errorResponse , ErrorResponse .class ));
123+ public void parseErrorAndThrow (
124+ @ Nonnull final String errorResponse , @ Nonnull final E baseException ) throws E {
125+ val maybeError = Try .of (() -> JACKSON .readValue (errorResponse , errorType ));
79126 if (maybeError .isFailure ()) {
80127 baseException .addSuppressed (maybeError .getCause ());
81128 throw baseException ;
82129 }
83130
84- throw new OrchestrationClientException (
131+ throw exceptionType . apply (
85132 "%s and error message: '%s'"
86- .formatted (baseException .getMessage (), maybeError .get ().getMessage ()));
87- }
88-
89- /**
90- * Processes a {@link ClassicHttpResponse} and returns some value corresponding to that response.
91- *
92- * @param response The response to process
93- * @return A model class instantiated from the response
94- * @throws OrchestrationClientException in case of a problem or the connection was aborted
95- */
96- @ Override
97- public T handleResponse (@ Nonnull final ClassicHttpResponse response )
98- throws OrchestrationClientException {
99- if (response .getCode () >= 300 ) {
100- buildExceptionAndThrow (response );
101- }
102- val result = parseResponse (response );
103- log .debug ("Received the following response from orchestration service: {}" , result );
104- return result ;
105- }
106-
107- // The InputStream of the HTTP entity is closed by EntityUtils.toString
108- @ SuppressWarnings ("PMD.CloseResource" )
109- @ Nonnull
110- private T parseResponse (@ Nonnull final ClassicHttpResponse response )
111- throws OrchestrationClientException {
112- final HttpEntity responseEntity = response .getEntity ();
113- if (responseEntity == null ) {
114- throw new OrchestrationClientException ("Response from Orchestration service was empty." );
115- }
116- val content = getContent (responseEntity );
117- log .debug ("Parsing response from JSON response: {}" , content );
118- try {
119- return JACKSON .readValue (content , responseType );
120- } catch (final JsonProcessingException e ) {
121- log .error ("Failed to parse the following response from orchestration service: {}" , content );
122- throw new OrchestrationClientException (
123- "Failed to parse response from orchestration service" , e );
124- }
133+ .formatted (baseException .getMessage (), maybeError .get ().getMessage ()),
134+ null );
125135 }
126136}
0 commit comments