Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Add logging to AbstractMethod.execute #559

Merged
merged 8 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading