Skip to content

Commit

Permalink
fix: Add logging to AbstractMethod.execute (#559)
Browse files Browse the repository at this point in the history
* fix: Add logging to AbstractMethod.execute

* Fix NPE

* refactor: Tidy up AbstractMethod docs / logs

* fix: Set AM log level to FINE

* test: Attempt to fix class init

* fix: Add logging to DynamicEndpoint

* test: Fix class initialisation bug

* Add missing logs in DynamicEndpoint
  • Loading branch information
SMadani authored Jan 9, 2025
1 parent 945d358 commit cd8527d
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 65 deletions.
144 changes: 107 additions & 37 deletions src/main/java/com/vonage/client/AbstractMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package com.vonage.client;

import com.vonage.client.auth.*;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
Expand All @@ -26,78 +26,143 @@
import java.nio.charset.StandardCharsets;
import java.util.AbstractMap;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
* Abstract class to assist in implementing a call against a REST endpoint.
* <p>
* Concrete implementations must implement {@link #makeRequest(Object)} to construct a {@link RequestBuilder} from the
* provided parameterized request object, and {@link #parseResponse(HttpResponse)} to construct the parameterized {@link
* HttpResponse} object.
* provided parameterized request object, and {@link #parseResponse(HttpResponse)} to construct the parameterized
* {@link HttpResponse} object.
* <p>
* The REST call is executed by calling {@link #execute(Object)}.
*
* @param <RequestT> The type of the method-specific request object that will be used to construct an HTTP request
* @param <ResultT> The type of method-specific response object which will be constructed from the returned HTTP
* response
* @param <REQ> The request object type that will be used to construct the HTTP request body.
* @param <RES> The response object type which will be constructed from the returned HTTP response body.
*
* @see DynamicEndpoint for a flexible implementation which handles the most common use cases.
*/
public abstract class AbstractMethod<RequestT, ResultT> implements RestEndpoint<RequestT, ResultT> {
static {
LogFactory.getLog(AbstractMethod.class);
public abstract class AbstractMethod<REQ, RES> implements RestEndpoint<REQ, RES> {
private static final Logger LOGGER = Logger.getLogger(AbstractMethod.class.getName());
private static final Level LOG_LEVEL = Level.FINE;

private static boolean shouldLog() {
return LOGGER.isLoggable(LOG_LEVEL);
}

protected final HttpWrapper httpWrapper;

public AbstractMethod(HttpWrapper httpWrapper) {
/**
* HTTP client and configuration used by this endpoint.
*/
private final HttpWrapper httpWrapper;

/**
* Construct a new AbstractMethod instance with the given HTTP client.
*
* @param httpWrapper The wrapper containing the HTTP client and configuration.
*/
protected AbstractMethod(HttpWrapper httpWrapper) {
this.httpWrapper = httpWrapper;
}

/**
* Gets the underlying HTTP client wrapper.
*
* @return The {@link HttpWrapper} used by this endpoint.
*/
public HttpWrapper getHttpWrapper() {
return httpWrapper;
}

protected ResultT postProcessParsedResponse(ResultT response) {
/**
* Method which allows further modification of the response object after it has been parsed.
*
* @param response The unmarshalled response object.
*
* @return The final result object to return; usually the same object that was passed in.
*/
protected RES postProcessParsedResponse(RES response) {
return response;
}

private HttpUriRequest createFullHttpRequest(REQ request) throws VonageClientException {
return applyAuth(makeRequest(request))
.setHeader(HttpHeaders.USER_AGENT, httpWrapper.getUserAgent())
.setCharset(StandardCharsets.UTF_8).build();
}

/**
* Execute the REST call represented by this method object.
* Executes the REST call represented by this endpoint.
*
* @param request A RequestT representing input to the REST call to be made
* @param request The request object representing input to the REST call to be made.
*
* @return A ResultT representing the response from the executed REST call
* @return The result object representing the response from the executed REST call.
*
* @throws VonageClientException if there is a problem parsing the HTTP response
* @throws VonageResponseParseException if there was a problem parsing the HTTP response.
* @throws VonageMethodFailedException if there was a problem executing the HTTP request.
*/
@Override
public ResultT execute(RequestT request) throws VonageResponseParseException, VonageClientException {
HttpUriRequest httpRequest = applyAuth(makeRequest(request))
.setHeader(HttpHeaders.USER_AGENT, httpWrapper.getUserAgent())
.setCharset(StandardCharsets.UTF_8).build();
public RES execute(REQ request) throws VonageMethodFailedException, VonageResponseParseException {
final HttpUriRequest httpRequest = createFullHttpRequest(request);

try (CloseableHttpResponse response = httpWrapper.getHttpClient().execute(httpRequest)) {
if (shouldLog()) {
LOGGER.log(LOG_LEVEL, "Request " + httpRequest.getMethod() + " " + httpRequest.getURI());
Header[] headers = httpRequest.getAllHeaders();
if (headers != null && headers.length > 0) {
StringBuilder headersStr = new StringBuilder("--- REQUEST HEADERS ---");
for (Header header : headers) {
headersStr.append('\n').append(header.getName()).append(": ").append(header.getValue());
}
LOGGER.log(LOG_LEVEL, headersStr.toString());
}
if (request != null) {
LOGGER.log(LOG_LEVEL, "--- REQUEST BODY ---\n" + request);
}
}

try (final CloseableHttpResponse response = httpWrapper.getHttpClient().execute(httpRequest)) {
try {
return postProcessParsedResponse(parseResponse(response));
if (shouldLog()) {
LOGGER.log(LOG_LEVEL, "Response " + response.getStatusLine());
Header[] headers = response.getAllHeaders();
if (headers != null && headers.length > 0) {
StringBuilder headersStr = new StringBuilder("--- RESPONSE HEADERS ---");
for (Header header : headers) {
headersStr.append('\n').append(header.getName()).append(": ").append(header.getValue());
}
LOGGER.log(LOG_LEVEL, headersStr.toString());
}
}

final RES responseBody = parseResponse(response);
if (responseBody != null && shouldLog()) {
LOGGER.log(LOG_LEVEL, "--- RESPONSE BODY ---\n" + responseBody);
}

return postProcessParsedResponse(responseBody);
}
catch (IOException iox) {
LOGGER.log(Level.WARNING, "Failed to parse response", iox);
throw new VonageResponseParseException(iox);
}
}
catch (IOException iox) {
LOGGER.log(Level.WARNING, "Failed to execute HTTP request", iox);
throw new VonageMethodFailedException("Something went wrong while executing the HTTP request.", iox);
}
}

/**
* Apply an appropriate authentication method (specified by {@link #getAcceptableAuthMethods()}) to the provided
* {@link RequestBuilder}, and return the result.
* Apply an appropriate authentication method (specified by {@link #getAcceptableAuthMethods()}) to the
* provided {@link RequestBuilder}, and return the result.
*
* @param request A RequestBuilder which has not yet had authentication information applied
* @param request A RequestBuilder which has not yet had authentication information applied.
*
* @return A RequestBuilder with appropriate authentication information applied (may or not be the same instance as
* <pre>request</pre>)
* @return A RequestBuilder with appropriate authentication information applied
* (may or not be the same instance as <pre>request</pre>).
*
* @throws VonageClientException If no appropriate {@link AuthMethod} is available
* @throws VonageClientException If no appropriate {@link AuthMethod} is available.
*/
final RequestBuilder applyAuth(RequestBuilder request) throws VonageClientException {
AuthMethod am = getAuthMethod();
Expand Down Expand Up @@ -127,25 +192,30 @@ protected AuthMethod getAuthMethod() throws VonageUnexpectedException {
return httpWrapper.getAuthCollection().getAcceptableAuthMethod(getAcceptableAuthMethods());
}

/**
* Gets applicable authentication methods for this endpoint.
*
* @return The set of acceptable authentication method classes (at least one must be provided).
*/
protected abstract Set<Class<? extends AuthMethod>> getAcceptableAuthMethods();

/**
* Construct and return a RequestBuilder instance from the provided request.
*
* @param request A RequestT representing input to the REST call to be made
* @param request A request object representing input to the REST call to be made.
*
* @return A ResultT representing the response from the executed REST call
* @return A RequestBuilder instance representing the HTTP request to be made.
*/
protected abstract RequestBuilder makeRequest(RequestT request);
protected abstract RequestBuilder makeRequest(REQ request);

/**
* Construct a ResultT representing the contents of the HTTP response returned from the Vonage Voice API.
* Construct a response object representing the contents of the HTTP response returned from the Vonage API.
*
* @param response An HttpResponse returned from the Vonage Voice API
* @param response An HttpResponse returned from the Vonage API.
*
* @return A ResultT type representing the result of the REST call
* @return The unmarshalled result of the REST call.
*
* @throws IOException if a problem occurs parsing the response
* @throws IOException if a problem occurs parsing the response.
*/
protected abstract ResultT parseResponse(HttpResponse response) throws IOException;
protected abstract RES parseResponse(HttpResponse response) throws IOException;
}
24 changes: 21 additions & 3 deletions src/main/java/com/vonage/client/DynamicEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Enables convenient declaration of endpoints without directly implementing {@link AbstractMethod}.
Expand All @@ -42,6 +44,8 @@
*/
@SuppressWarnings("unchecked")
public class DynamicEndpoint<T, R> extends AbstractMethod<T, R> {
protected final Logger logger = Logger.getLogger(getClass().getName());

protected Set<Class<? extends AuthMethod>> authMethods;
protected String contentType, accept;
protected HttpMethod requestMethod;
Expand Down Expand Up @@ -242,6 +246,7 @@ else if (requestBody instanceof byte[]) {
@Override
protected final R parseResponse(HttpResponse response) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
logger.fine(() -> "Response status: " + statusCode);
try {
if (statusCode >= 200 && statusCode < 300) {
return parseResponseSuccess(response);
Expand All @@ -255,6 +260,7 @@ else if (statusCode >= 300 && statusCode < 400) {
}
catch (InvocationTargetException ex) {
Throwable wrapped = ex.getTargetException();
logger.log(Level.SEVERE, "Internal SDK error", ex);
if (wrapped instanceof RuntimeException) {
throw (RuntimeException) wrapped;
}
Expand All @@ -263,6 +269,7 @@ else if (statusCode >= 300 && statusCode < 400) {
}
}
catch (ReflectiveOperationException ex) {
logger.log(Level.SEVERE, "Internal SDK error", ex);
throw new VonageUnexpectedException(ex);
}
finally {
Expand All @@ -276,6 +283,7 @@ protected R parseResponseFromString(String response) {

private R parseResponseRedirect(HttpResponse response) throws ReflectiveOperationException, IOException {
final String location = response.getFirstHeader("Location").getValue();
logger.fine(() -> "Redirect: " + location);

if (java.net.URI.class.equals(responseType)) {
return (R) URI.create(location);
Expand All @@ -290,13 +298,17 @@ else if (String.class.equals(responseType)) {

private R parseResponseSuccess(HttpResponse response) throws IOException, ReflectiveOperationException {
if (Void.class.equals(responseType)) {
logger.fine(() -> "No response body.");
return null;
}
else if (byte[].class.equals(responseType)) {
return (R) EntityUtils.toByteArray(response.getEntity());
byte[] result = EntityUtils.toByteArray(response.getEntity());
logger.fine(() -> "Binary response body of length " + result.length);
return (R) result;
}
else {
String deser = EntityUtils.toString(response.getEntity());
logger.fine(() -> deser);

if (responseType.equals(String.class)) {
return (R) deser;
Expand All @@ -316,7 +328,9 @@ else if (Collection.class.isAssignableFrom(responseType) || isJsonableArrayRespo
else {
R customParsedResponse = parseResponseFromString(deser);
if (customParsedResponse == null) {
throw new IllegalStateException("Unhandled return type: " + responseType);
String errorMsg = "Unhandled return type: " + responseType;
logger.severe(errorMsg);
throw new IllegalStateException(errorMsg);
}
else {
return customParsedResponse;
Expand All @@ -336,6 +350,7 @@ private R parseResponseFailure(HttpResponse response) throws IOException, Reflec
varex.title = response.getStatusLine().getReasonPhrase();
}
varex.statusCode = response.getStatusLine().getStatusCode();
logger.log(Level.WARNING, "Failed to parse response", varex);
throw varex;
}
else {
Expand All @@ -345,13 +360,16 @@ private R parseResponseFailure(HttpResponse response) throws IOException, Reflec
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
throw (RuntimeException) constructor.newInstance(exMessage);
RuntimeException ex = (RuntimeException) constructor.newInstance(exMessage);
logger.log(Level.SEVERE, "Internal SDK error", ex);
throw ex;
}
}
}
}
R customParsedResponse = parseResponseFromString(exMessage);
if (customParsedResponse == null) {
logger.warning(exMessage);
throw new VonageApiResponseException(exMessage);
}
else {
Expand Down
Loading

0 comments on commit cd8527d

Please sign in to comment.