From 24c464bbc4eedb5d1fa231b79cbe150f3f3942be Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 11:31:06 +0200 Subject: [PATCH 01/11] Remove obsolete handlers first. #7134 --- .../BadRequestExceptionHandler.java | 49 ------------------- .../ForbiddenExceptionHandler.java | 36 -------------- .../InternalServerErrorExceptionHandler.java | 39 --------------- .../NotAllowedExceptionHandler.java | 31 ------------ .../NotFoundExceptionHandler.java | 43 ---------------- .../RedirectionExceptionHandler.java | 32 ------------ .../ServiceUnavailableExceptionHandler.java | 43 ---------------- 7 files changed, 273 deletions(-) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/BadRequestExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ForbiddenExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/InternalServerErrorExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotAllowedExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotFoundExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/RedirectionExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServiceUnavailableExceptionHandler.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/BadRequestExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/BadRequestExceptionHandler.java deleted file mode 100644 index f6412a871ac..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/BadRequestExceptionHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import edu.harvard.iq.dataverse.util.BundleUtil; -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * - * @author skraffmi - */ -@Provider -public class BadRequestExceptionHandler implements ExceptionMapper { - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(BadRequestException ex) { - System.out.print( ex.getMessage()); - String uri = request.getRequestURI(); - String exMessage = ex.getMessage(); - String outputMessage; - if (exMessage != null && exMessage.toLowerCase().startsWith("tabular data required")) { - outputMessage = BundleUtil.getStringFromBundle("access.api.exception.metadata.not.available.for.nontabular.file"); - } else { - outputMessage = "Bad Request. The API request cannot be completed with the parameters supplied. Please check your code for typos, or consult our API guide at http://guides.dataverse.org."; - } - return Response.status(400) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 400) - .add("message", "'" + uri + "' " + outputMessage) - .build()) - .type("application/json").build(); - - - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ForbiddenExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ForbiddenExceptionHandler.java deleted file mode 100644 index 8096b719832..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ForbiddenExceptionHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 403 messages for the API. - * @author michael - */ -@Provider -public class ForbiddenExceptionHandler implements ExceptionMapper{ - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(ForbiddenException ex){ - String uri = request.getRequestURI(); - return Response.status(403) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 403) - .add("message", "'" + uri + "' you are not authorized to access this object via this api endpoint. Please check your code for typos, or consult our API guide at http://guides.dataverse.org.") - .build()) - .type("application/json").build(); - - - } - -} - diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/InternalServerErrorExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/InternalServerErrorExceptionHandler.java deleted file mode 100644 index f59dd5029fc..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/InternalServerErrorExceptionHandler.java +++ /dev/null @@ -1,39 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 500 messages for the API. - * @author qqmyers - */ -@Provider -public class InternalServerErrorExceptionHandler implements ExceptionMapper{ - - private static final Logger logger = Logger.getLogger(InternalServerErrorExceptionHandler.class.getName()); - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(InternalServerErrorException ex){ - String incidentId = UUID.randomUUID().toString(); - logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex); - return Response.status(500) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 500) - .add("message", "Internal server error. More details available at the server logs.") - .add("incidentId", incidentId) - .build()) - .type("application/json").build(); - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotAllowedExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotAllowedExceptionHandler.java deleted file mode 100644 index 5df16c9596d..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotAllowedExceptionHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.NotAllowedException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -@Provider -public class NotAllowedExceptionHandler implements ExceptionMapper{ - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(NotAllowedException ex){ - String uri = request.getRequestURI(); - return Response.status(405) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 405) - .add("message", "'" + uri + "' endpoint does not support method '"+request.getMethod()+"'. Consult our API guide at http://guides.dataverse.org.") - .build()) - .type("application/json").build(); - - - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotFoundExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotFoundExceptionHandler.java deleted file mode 100644 index 51c4f343f85..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NotFoundExceptionHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 404 messages for the API. - * @author michael - */ -@Provider -public class NotFoundExceptionHandler implements ExceptionMapper{ - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(NotFoundException ex){ - String uri = request.getRequestURI(); - String exMessage = ex.getMessage(); - String outputMessage; - if (exMessage != null && exMessage.toLowerCase().startsWith("datafile")) { - outputMessage = exMessage; - } else { - outputMessage = "endpoint does not exist on this server. Please check your code for typos, or consult our API guide at http://guides.dataverse.org."; - } - return Response.status(404) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 404) - .add("message", "'" + uri + "' " + outputMessage) - .build()) - .type("application/json").build(); - - - } - -} - diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/RedirectionExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/RedirectionExceptionHandler.java deleted file mode 100644 index 21e677180be..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/RedirectionExceptionHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import edu.harvard.iq.dataverse.util.BundleUtil; -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.RedirectionException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * - * @author qqmyers - */ -@Provider -public class RedirectionExceptionHandler implements ExceptionMapper { - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(RedirectionException ex) { - return ex.getResponse(); - } - -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServiceUnavailableExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServiceUnavailableExceptionHandler.java deleted file mode 100644 index ebed03a13da..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServiceUnavailableExceptionHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.ServiceUnavailableException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 503 messages for the API. - * @author michael - */ -@Provider -public class ServiceUnavailableExceptionHandler implements ExceptionMapper{ - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(ServiceUnavailableException ex){ - String uri = request.getRequestURI(); - String exMessage = ex.getMessage(); - String outputMessage; - if (exMessage != null && exMessage.toLowerCase().startsWith("datafile")) { - outputMessage = exMessage; - } else { - outputMessage = "Requested service or method not available on the requested object"; - } - return Response.status(503) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 503) - .add("message", "'" + uri + "' " + outputMessage) - .build()) - .type("application/json").build(); - - - } - -} - From 5961abff9c487713481426cc5e482a4443112839 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 12:55:16 +0200 Subject: [PATCH 02/11] Refactor ThrowableHandler to provide a common set of logging and response formating. --- .../api/errorhandlers/ThrowableHandler.java | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java index 8514d099787..7b6b99daa4e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -1,6 +1,5 @@ package edu.harvard.iq.dataverse.api.errorhandlers; -import javax.annotation.Priority; import javax.json.Json; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Context; @@ -8,9 +7,12 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; +import java.util.Map; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Produces a generic 500 message for the API, being a fallback handler for not specially treated exceptions. @@ -41,7 +43,88 @@ public Response toResponse(Throwable ex){ .type(MediaType.APPLICATION_JSON_TYPE).build(); } - private String getOriginalURL(HttpServletRequest req) { + /** + * Some HTTP return codes should be logged at higher levels than FINEST (which is for debugging). + * Match the code and a log level here. + */ + static Map levels = Stream.of(new Object[][] { + {500, Level.SEVERE} + }).collect(Collectors.toMap(data -> (Integer) data[0], data -> (Level) data[1])); + + /** + * Create a useful log message with parseable metadata and log it. + * Log level of message is determined by the levels map and defaults to FINEST (=debugging). + * Log messages will only be logged if the level is enabled for the logger. + * @param header A useful, human readable error message. + * @param requestMethod The HTTP method that triggered the error + * @param requestUrl The HTTP request URL that triggered the error + * @param httpStatus The response HTTP status code + * @param ex The real exception that has been thrown + * @return A random incident id used in the log message, may be forwarded to the client response message. + */ + static String handleLogging(String header, String requestMethod, String requestUrl, int httpStatus, Throwable ex) { + String incidentId = UUID.randomUUID().toString(); + String message = "error="+ex.getClass().getSimpleName()+ + ";incident="+incidentId+ + ";method="+requestMethod+ + ";url="+requestUrl+ + ";status="+httpStatus+"|\n"+ + header+"|"; + + Level level = levels.getOrDefault(httpStatus, Level.FINEST); + if (logger.isLoggable(level)) + logger.log(level, message, ex); + + return incidentId; + } + + /** + * Create a new (error) response with a human readable yet machine actionable JSON error message. + * @param status HTTP return code + * @param requestMethod The HTTP method used in the original request + * @param requestUrl The HTTP URL used in the original request + * @param errorType Ideally a exception class name, but maybe free text + * @param message A human readable message + * @param incidentId A random incident id. Should be the same as the logged id for easier error analysis + * @return A response ready to be send to the client + */ + static Response createErrorResponse(int status, String requestMethod, String requestUrl, String errorType, String message, String incidentId) { + return createErrorResponse(Response.status(status).build(), requestMethod, requestUrl, errorType, message, incidentId); + } + + /** + * Create an error response with a human readable yet machine actionable JSON error message, based on an existing + * response (necessary for things like a redirect). + * @param fromResponse The original response to enhance + * @param requestMethod The HTTP method used in the original request + * @param requestUrl The HTTP URL used in the original request + * @param errorType Ideally a exception class name, but maybe free text + * @param message A human readable message + * @param incidentId A random incident id. Should be the same as the logged id for easier error analysis + * @return A response ready to be send to the client + */ + static Response createErrorResponse(Response fromResponse, String requestMethod, String requestUrl, String errorType, String message, String incidentId) { + return Response.fromResponse(fromResponse) + .entity(Json.createObjectBuilder() + .add("status", "ERROR") + .add("code", fromResponse.getStatus()) + .add("method", requestMethod) + .add("url", requestUrl) + .add("errorType", errorType) + .add("message", message) + .add("incidentId", incidentId) + .build() + ) + .type(MediaType.APPLICATION_JSON_TYPE) + .build(); + } + + /** + * Build a complete request URL for logging purposes + * @param req The request + * @return The requests URL sent by the client + */ + static String getOriginalURL(HttpServletRequest req) { // Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408 String scheme = req.getScheme(); // http String serverName = req.getServerName(); // hostname.com From 7ebf241542b7184060bd6959a7fd4c1471fa5cae Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 12:59:35 +0200 Subject: [PATCH 03/11] Dogfooding the new methods to the ThrowableHandler toResponse(). #7134 --- .../api/errorhandlers/ThrowableHandler.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java index 7b6b99daa4e..c207e64bc83 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -21,26 +21,28 @@ public class ThrowableHandler implements ExceptionMapper{ private static final Logger logger = Logger.getLogger(ThrowableHandler.class.getName()); + static final String INTERNAL_SERVER_ERROR_MESSAGE = "Internal server error. More details available at the server logs."; @Context HttpServletRequest request; @Override public Response toResponse(Throwable ex){ - String incidentId = UUID.randomUUID().toString(); - logger.log(Level.SEVERE, "Uncaught REST API exception:\n"+ - " Incident: " + incidentId +"\n"+ - " URL: "+getOriginalURL(request)+"\n"+ - " Method: "+request.getMethod(), ex); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) - .add("type", ex.getClass().getSimpleName()) - .add("message", "Internal server error. More details available at the server logs.") - .add("incidentId", incidentId) - .build()) - .type(MediaType.APPLICATION_JSON_TYPE).build(); + String requestMethod = request.getMethod(); + String requestUrl = ThrowableHandler.getOriginalURL(request); + + String incidentId = ThrowableHandler.handleLogging(INTERNAL_SERVER_ERROR_MESSAGE, + requestMethod, + requestUrl, + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), + ex); + + return ThrowableHandler.createErrorResponse(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), + requestMethod, + requestUrl, + ex.getClass().getSimpleName(), + INTERNAL_SERVER_ERROR_MESSAGE, + incidentId); } /** From 3584e19e7c1b151cf3ffe82f3d94ca5a23d41ca2 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 13:02:49 +0200 Subject: [PATCH 04/11] Finally implement the WebApplicationExceptionHandler, reducing duplicated code and solve problems like #7134. --- .../WebApplicationExceptionHandler.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java new file mode 100644 index 00000000000..924450946c5 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java @@ -0,0 +1,108 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package edu.harvard.iq.dataverse.api.errorhandlers; + +import edu.harvard.iq.dataverse.util.BundleUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.logging.Logger; + +/** + * Catches all types of web application exceptions like NotFoundException, etc etc and handles them properly. + */ +@Provider +public class WebApplicationExceptionHandler implements ExceptionMapper { + + static final Logger logger = Logger.getLogger(WebApplicationExceptionHandler.class.getSimpleName()); + + @Context + HttpServletRequest request; + + @Override + public Response toResponse(WebApplicationException ex) { + + String requestMethod = request.getMethod(); + String requestUrl = ThrowableHandler.getOriginalURL(request); + String message = createMessage(ex); + + String incidentId = ThrowableHandler.handleLogging(message, + requestMethod, + requestUrl, + ex.getResponse().getStatus(), + ex); + + return ThrowableHandler.createErrorResponse(ex.getResponse(), + requestMethod, + requestUrl, + ex.getClass().getSimpleName(), + message, + incidentId); + } + + /** + * Analyse the exception and generate a human readable (and helpful) error message. + * @param ex The exception thrown + * @return A human readable error message. + */ + String createMessage(WebApplicationException ex) { + + String message = ""; + String exMessage = ex.getMessage(); + + // See also https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for a list of status codes. + switch (ex.getResponse().getStatus()) { + // Redirects (permanent & temporary) + case 302: + case 307: + message = ex.getResponse().getLocation().toString(); + break; + // BadRequest + case 400: + if (exMessage != null && exMessage.toLowerCase().startsWith("tabular data required")) { + message = BundleUtil.getStringFromBundle("access.api.exception.metadata.not.available.for.nontabular.file"); + } else { + message = "Bad Request. The API request cannot be completed with the parameters supplied. Please check your code for typos, or consult our API guide at http://guides.dataverse.org."; + } + break; + // Forbidden + case 403: + message = "Not authorized to access this object via this API endpoint. Please check your code for typos, or consult our API guide at http://guides.dataverse.org."; + break; + // NotFound + case 404: + if (exMessage != null && exMessage.toLowerCase().startsWith("datafile")) { + message = exMessage; + } else { + message = "API endpoint does not exist on this server. Please check your code for typos, or consult our API guide at http://guides.dataverse.org."; + } + break; + // MethodNotAllowed + case 405: + message = "API endpoint does not support this method. Consult our API guide at http://guides.dataverse.org."; + break; + // InternalServerError + case 500: + message = "Internal server error. More details available at the server logs. Please contact your dataverse administrator."; + break; + // ServiceUnavailable + case 503: + if (exMessage != null && exMessage.toLowerCase().startsWith("datafile")) { + message = exMessage; + } else { + message = "Requested service or method not available on the requested object"; + } + break; + } + + return message; + } + +} From eec06412ccd7be8874d6a0f2a8d02300a0d641e6 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 13:08:36 +0200 Subject: [PATCH 05/11] Further reduce boilerplate and duplicated code by removing handlers for ArrayOutOfBoundsException, NullPointerException and ServeletException perfectly replaced by ThrowableHandler. #7134 --- .../ArrayOutOfBoundsExceptionHandler.java | 38 ------------------- .../NullPointerExceptionHandler.java | 38 ------------------- .../ServeletExceptionHandler.java | 38 ------------------- .../api/errorhandlers/ThrowableHandler.java | 3 ++ 4 files changed, 3 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ArrayOutOfBoundsExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NullPointerExceptionHandler.java delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServeletExceptionHandler.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ArrayOutOfBoundsExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ArrayOutOfBoundsExceptionHandler.java deleted file mode 100644 index 112f50eb3ed..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ArrayOutOfBoundsExceptionHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 500 messages for the API. - * @author michael - */ -@Provider -public class ArrayOutOfBoundsExceptionHandler implements ExceptionMapper{ - - private static final Logger logger = Logger.getLogger(ServeletExceptionHandler.class.getName()); - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(java.lang.ArrayIndexOutOfBoundsException ex){ - String incidentId = UUID.randomUUID().toString(); - logger.log(Level.SEVERE, "API internal error " + incidentId +": ArrayOutOfBounds:" + ex.getMessage(), ex); - return Response.status(500) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 500) - .add("message", "Internal server error. More details available at the server logs.") - .add("incidentId", incidentId) - .build()) - .type("application/json").build(); - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NullPointerExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NullPointerExceptionHandler.java deleted file mode 100644 index 1e9c2a28690..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/NullPointerExceptionHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 500 messages for the API. - * @author michael - */ -@Provider -public class NullPointerExceptionHandler implements ExceptionMapper{ - - private static final Logger logger = Logger.getLogger(ServeletExceptionHandler.class.getName()); - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(java.lang.NullPointerException ex){ - String incidentId = UUID.randomUUID().toString(); - logger.log(Level.SEVERE, "API internal error " + incidentId +": Null Pointer", ex); - return Response.status(500) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 500) - .add("message", "Internal server error. More details available at the server logs.") - .add("incidentId", incidentId) - .build()) - .type("application/json").build(); - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServeletExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServeletExceptionHandler.java deleted file mode 100644 index fa6bff57c03..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ServeletExceptionHandler.java +++ /dev/null @@ -1,38 +0,0 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; - -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.json.Json; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * Produces custom 500 messages for the API. - * @author michael - */ -@Provider -public class ServeletExceptionHandler implements ExceptionMapper{ - - private static final Logger logger = Logger.getLogger(ServeletExceptionHandler.class.getName()); - - @Context - HttpServletRequest request; - - @Override - public Response toResponse(javax.servlet.ServletException ex){ - String incidentId = UUID.randomUUID().toString(); - logger.log(Level.SEVERE, "API internal error " + incidentId +": " + ex.getMessage(), ex); - return Response.status(500) - .entity( Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", 500) - .add("message", "Internal server error. More details available at the server logs.") - .add("incidentId", incidentId) - .build()) - .type("application/json").build(); - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java index c207e64bc83..9926a244f4d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -16,6 +16,9 @@ /** * Produces a generic 500 message for the API, being a fallback handler for not specially treated exceptions. + * + * This catches bad exceptions like ArrayOutOfBoundsExceptions, NullPointerExceptions and ServeletExceptions, + * which had formerly specialised handlers, generating a generic error message. (This is now handled here.) */ @Provider public class ThrowableHandler implements ExceptionMapper{ From c9bca3a8a601ae6eda24d2ced0589950d7c1c427 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 13:32:33 +0200 Subject: [PATCH 06/11] Mask unblock-key query parameter in logs and responses to avoid cleartext storage of secrets. #7134 --- .../iq/dataverse/api/ApiBlockingFilter.java | 2 +- .../api/errorhandlers/ThrowableHandler.java | 6 +++- .../errorhandlers/ThrowableHandlerTest.java | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java diff --git a/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java b/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java index 761a6c8ef77..a0f584736f1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/ApiBlockingFilter.java @@ -25,7 +25,7 @@ * @author michael */ public class ApiBlockingFilter implements javax.servlet.Filter { - private static final String UNBLOCK_KEY_QUERYPARAM = "unblock-key"; + public static final String UNBLOCK_KEY_QUERYPARAM = "unblock-key"; interface BlockPolicy { public void doBlock(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java index 9926a244f4d..20e3753d22d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -1,5 +1,7 @@ package edu.harvard.iq.dataverse.api.errorhandlers; +import edu.harvard.iq.dataverse.api.ApiBlockingFilter; + import javax.json.Json; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Context; @@ -150,7 +152,9 @@ static String getOriginalURL(HttpServletRequest req) { url.append(pathInfo); } if (queryString != null) { - url.append("?").append(queryString); + // filter for unblock-key parameter and mask the key + String maskedQueryString = queryString.replaceAll(ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=.+?\\b", ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=****"); + url.append("?").append(maskedQueryString); } return url.toString(); diff --git a/src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java b/src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java new file mode 100644 index 00000000000..50b92ba3ce7 --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java @@ -0,0 +1,32 @@ +package edu.harvard.iq.dataverse.api.errorhandlers; + +import edu.harvard.iq.dataverse.api.ApiBlockingFilter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +import javax.servlet.http.HttpServletRequest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class ThrowableHandlerTest { + + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings = { + "test", + ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=supersecret", + ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=supersecret&hello=1234", + "hello=1234&"+ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=supersecret", + "hello=1234&"+ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=supersecret&test=1234"}) + void testMaskingOriginalURL(String query) { + HttpServletRequest test = Mockito.mock(HttpServletRequest.class); + when(test.getQueryString()).thenReturn(query); + assertFalse(ThrowableHandler.getOriginalURL(test).contains("supersecret")); + } +} \ No newline at end of file From ead241d54e951668d1a5e1edf3d7ae1daab652a5 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Tue, 28 Jul 2020 14:02:23 +0200 Subject: [PATCH 07/11] Add default message from Jersey WebApplicationException message to JSON responses. #7134 --- .../api/errorhandlers/WebApplicationExceptionHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java index 924450946c5..83b6aaaa33c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java @@ -100,6 +100,9 @@ String createMessage(WebApplicationException ex) { message = "Requested service or method not available on the requested object"; } break; + default: + message = ex.getMessage(); + break; } return message; From e17a1ee21458ec55297101fe983eb15de2dfe340 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 29 Jul 2020 14:49:18 +0200 Subject: [PATCH 08/11] Create a new unified JSON responses and logging infrastructure for the API. #7134 Only supports error responses for now. --- .../api/util/JSONResponseBuilder.java | 267 ++++++++++++++++++ .../JSONResponseBuilderTest.java} | 8 +- 2 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java rename src/test/java/edu/harvard/iq/dataverse/api/{errorhandlers/ThrowableHandlerTest.java => util/JSONResponseBuilderTest.java} (81%) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java b/src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java new file mode 100644 index 00000000000..eea46eada2e --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java @@ -0,0 +1,267 @@ +package edu.harvard.iq.dataverse.api.util; + +import edu.harvard.iq.dataverse.api.ApiBlockingFilter; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class JSONResponseBuilder { + + private JsonObjectBuilder entityBuilder = Json.createObjectBuilder(); + private Response.ResponseBuilder jerseyResponseBuilder; + private boolean alreadyLogged = false; + + private JSONResponseBuilder() {} + + /** + * Create an error response from an numeric error code (should be >= 400) + * @param httpStatusCode Numerical HTTP status code + * @return A builder with a basic JSON body + * @throws IllegalArgumentException if fromResponse isn't an error response + */ + public static JSONResponseBuilder error(int httpStatusCode) { + if (httpStatusCode < 400) { + throw new IllegalArgumentException("A status code < 400 cannot be an error indicating response."); + } + + JSONResponseBuilder b = new JSONResponseBuilder(); + b.jerseyResponseBuilder = Response.status(httpStatusCode); + b.entityBuilder.add("status", "ERROR"); + b.entityBuilder.add("code", httpStatusCode); + // include default message if present + getDefaultMessage(httpStatusCode).ifPresent(v -> b.entityBuilder.add("message", v)); + + return b; + } + + /** + * Create an error response from a Response.Status. + * @param status A JAX-RS Response.Status object + * @return A builder with a basic JSON body + * @throws IllegalArgumentException if fromResponse isn't an error response + */ + public static JSONResponseBuilder error(Response.Status status) { + JSONResponseBuilder b = error(status.getStatusCode()); + b.jerseyResponseBuilder = Response.status(status); + return b; + } + + /** + * Create an error response from an existing response. + * @param fromResponse An existing JAX-RS Response + * @return A builder with a basic JSON body + * @throws IllegalArgumentException if fromResponse isn't an error response + */ + public static JSONResponseBuilder error(Response fromResponse) { + JSONResponseBuilder b = error(fromResponse.getStatus()); + b.jerseyResponseBuilder = Response.fromResponse(fromResponse); + return b; + } + + /** + * Add a human friendly message to the response + * @param message A human readable message + * @return The enhanced builder + */ + public JSONResponseBuilder message(String message) { + this.entityBuilder.add("message", message); + return this; + } + + /** + * Set an identifier for this (usually included in logs, too). + * @param id A String containing an (ideally unique) identifier + * @return The enhanced builder + */ + public JSONResponseBuilder incidentId(String id) { + this.entityBuilder.add("incidentId", id); + return this; + } + + /** + * Add a UUID random identifier for errors. Will overwrite existing. + * @return The enhanced builder + */ + public JSONResponseBuilder randomIncidentId() { + this.entityBuilder.add("incidentId", UUID.randomUUID().toString()); + return this; + } + + /** + * Add more details about the original request: what URL was used, + * what HTTP method involved? + * @param request The original request (usually provided from a context) + * @return The enhanced builder + */ + public JSONResponseBuilder request(HttpServletRequest request) { + this.entityBuilder.add("requestUrl", getOriginalURL(request)); + this.entityBuilder.add("requestMethod", request.getMethod()); + return this; + } + + /** + * Add more details about the original request: what content type was sent? + * @param request The original request (usually provided from a context) + * @return The enhanced builder + */ + public JSONResponseBuilder requestContentType(HttpServletRequest request) { + this.entityBuilder.add("requestContentType", request.getContentType()); + return this; + } + + /** + * Add more details about internal errors (exceptions) to the response. + * Will include a detail about the cause if exception has one. + * @param ex An exception. + * @return The enhanced builder + */ + public JSONResponseBuilder internalError(Throwable ex) { + this.entityBuilder.add("interalError", ex.getClass().getSimpleName()); + if (ex.getCause() != null) { + this.entityBuilder.add("internalCause", ex.getCause().getClass().getSimpleName()); + } + return this; + } + + /** + * Finish building a Jersey JAX-RS response with JSON message + * @return JAX-RS response including JSON message + */ + public Response build() { + return jerseyResponseBuilder.type(MediaType.APPLICATION_JSON_TYPE) + .entity(this.entityBuilder.build()) + .build(); + } + + /** + * For usage in non-Jersey areas like servlet filters, blocks, etc., + * apply the response to the Servlet provided response object. + * @param response The ServletResponse from the context + * @throws IOException + */ + public void apply(ServletResponse response) throws IOException { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + apply(httpServletResponse); + } + + /** + * For usage in non-Jersey areas like servlet filters, blocks, etc., + * apply the response to the Servlet provided response object. + * @param response The HttpServletResponse from the context + * @throws IOException + */ + public void apply(HttpServletResponse response) throws IOException { + Response jersey = jerseyResponseBuilder.build(); + response.setStatus(jersey.getStatus()); + response.setContentType(MediaType.APPLICATION_JSON); + response.getWriter().print(entityBuilder.build().toString()); + response.getWriter().flush(); + } + + /** + * Log this JSON response as a useful log message. + * Should be done before calling build(), but after adding any decorations. + * + * The log message will contain a message with the flattened JSON entity: + * { "status": "ERROR", "code": 401 } -> _status=ERROR;_code=401 + * + * Will prevent logging the same response twice. + * + * @param logger Provide a logger instance to write to + * @param level Provide a level at which this should be logged + * @return The unmodified builder. + */ + public JSONResponseBuilder log(Logger logger, Level level) { + return this.log(logger, level, Optional.empty()); + } + + /** + * Log this JSON response as a useful log message. + * Should be done before calling build(), but after adding any decorations. + * + * The log message will contain a message with the flattened JSON entity: + * { "status": "ERROR", "code": 401 } -> _status=ERROR;_code=401 + * + * If an exception is given, it will be folled by a "|" and the exception message + * formatted by the logging system itself. + * + * Will prevent logging the same response twice. + * + * @param logger Provide a logger instance to write to + * @param level Provide a level at which this should be logged + * @param ex An optional exception to be included in the log message. + * @return The unmodified builder. + */ + public JSONResponseBuilder log(Logger logger, Level level, Optional ex) { + if ( ! logger.isLoggable(level) || alreadyLogged ) + return this; + + StringBuilder metadata = new StringBuilder(""); + this.entityBuilder.build() + .forEach((k,v) -> metadata.append("_"+k+"="+v.toString()+";")); + // remove trailing ; + metadata.deleteCharAt(metadata.length()-1); + + if (ex.isPresent()) { + metadata.append("|"); + logger.log(level, metadata.toString(), ex); + } else { + logger.log(level, metadata.toString()); + } + + this.alreadyLogged = true; + return this; + } + + /** + * Build a complete request URL for logging purposes. + * Masks query parameter "unblock-key" if present to avoid leaking secrets. + * @param req The request + * @return The requests URL sent by the client + */ + public static String getOriginalURL(HttpServletRequest req) { + // Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408 + String scheme = req.getScheme(); // http + String serverName = req.getServerName(); // hostname.com + int serverPort = req.getServerPort(); // 80 + String contextPath = req.getContextPath(); // /mywebapp + String servletPath = req.getServletPath(); // /servlet/MyServlet + String pathInfo = req.getPathInfo(); // /a/b;c=123 + String queryString = req.getQueryString(); // d=789 + + // Reconstruct original requesting URL + StringBuilder url = new StringBuilder(); + url.append(scheme).append("://").append(serverName); + if (serverPort != 80 && serverPort != 443) { + url.append(":").append(serverPort); + } + url.append(contextPath).append(servletPath); + if (pathInfo != null) { + url.append(pathInfo); + } + if (queryString != null) { + // filter for unblock-key parameter and mask the key + String maskedQueryString = queryString.replaceAll(ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=.+?\\b", ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=****"); + url.append("?").append(maskedQueryString); + } + + return url.toString(); + } + + public static Optional getDefaultMessage(int httpStatusCode) { + switch (httpStatusCode) { + case 500: return Optional.of("Internal server error. More details available at the server logs."); + default: return Optional.empty(); + } + } +} diff --git a/src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java b/src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java similarity index 81% rename from src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java rename to src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java index 50b92ba3ce7..29e0536f73c 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandlerTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java @@ -1,7 +1,7 @@ -package edu.harvard.iq.dataverse.api.errorhandlers; +package edu.harvard.iq.dataverse.api.util; import edu.harvard.iq.dataverse.api.ApiBlockingFilter; -import org.junit.jupiter.api.Test; +import edu.harvard.iq.dataverse.api.errorhandlers.ThrowableHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.NullSource; @@ -13,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; -class ThrowableHandlerTest { +class JSONResponseBuilderTest { @ParameterizedTest @NullSource @@ -27,6 +27,6 @@ class ThrowableHandlerTest { void testMaskingOriginalURL(String query) { HttpServletRequest test = Mockito.mock(HttpServletRequest.class); when(test.getQueryString()).thenReturn(query); - assertFalse(ThrowableHandler.getOriginalURL(test).contains("supersecret")); + assertFalse(JSONResponseBuilder.getOriginalURL(test).contains("supersecret")); } } \ No newline at end of file From 5193cce5f20de8a9b621e854a167d1404d9bb1be Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 29 Jul 2020 15:39:59 +0200 Subject: [PATCH 09/11] Switch ThrowableHandler and WebApplicationExceptionHandler to the new JSONResponseBuilder for crafting responses and logging. #7134 --- .../api/errorhandlers/ThrowableHandler.java | 137 +----------------- .../WebApplicationExceptionHandler.java | 85 +++++------ 2 files changed, 47 insertions(+), 175 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java index 20e3753d22d..74330c04c4d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -1,15 +1,14 @@ package edu.harvard.iq.dataverse.api.errorhandlers; -import edu.harvard.iq.dataverse.api.ApiBlockingFilter; +import edu.harvard.iq.dataverse.api.util.JSONResponseBuilder; -import javax.json.Json; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -26,137 +25,17 @@ public class ThrowableHandler implements ExceptionMapper{ private static final Logger logger = Logger.getLogger(ThrowableHandler.class.getName()); - static final String INTERNAL_SERVER_ERROR_MESSAGE = "Internal server error. More details available at the server logs."; @Context HttpServletRequest request; @Override public Response toResponse(Throwable ex){ - String requestMethod = request.getMethod(); - String requestUrl = ThrowableHandler.getOriginalURL(request); - - String incidentId = ThrowableHandler.handleLogging(INTERNAL_SERVER_ERROR_MESSAGE, - requestMethod, - requestUrl, - Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), - ex); - - return ThrowableHandler.createErrorResponse(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), - requestMethod, - requestUrl, - ex.getClass().getSimpleName(), - INTERNAL_SERVER_ERROR_MESSAGE, - incidentId); - } - - /** - * Some HTTP return codes should be logged at higher levels than FINEST (which is for debugging). - * Match the code and a log level here. - */ - static Map levels = Stream.of(new Object[][] { - {500, Level.SEVERE} - }).collect(Collectors.toMap(data -> (Integer) data[0], data -> (Level) data[1])); - - /** - * Create a useful log message with parseable metadata and log it. - * Log level of message is determined by the levels map and defaults to FINEST (=debugging). - * Log messages will only be logged if the level is enabled for the logger. - * @param header A useful, human readable error message. - * @param requestMethod The HTTP method that triggered the error - * @param requestUrl The HTTP request URL that triggered the error - * @param httpStatus The response HTTP status code - * @param ex The real exception that has been thrown - * @return A random incident id used in the log message, may be forwarded to the client response message. - */ - static String handleLogging(String header, String requestMethod, String requestUrl, int httpStatus, Throwable ex) { - String incidentId = UUID.randomUUID().toString(); - String message = "error="+ex.getClass().getSimpleName()+ - ";incident="+incidentId+ - ";method="+requestMethod+ - ";url="+requestUrl+ - ";status="+httpStatus+"|\n"+ - header+"|"; - - Level level = levels.getOrDefault(httpStatus, Level.FINEST); - if (logger.isLoggable(level)) - logger.log(level, message, ex); - - return incidentId; - } - - /** - * Create a new (error) response with a human readable yet machine actionable JSON error message. - * @param status HTTP return code - * @param requestMethod The HTTP method used in the original request - * @param requestUrl The HTTP URL used in the original request - * @param errorType Ideally a exception class name, but maybe free text - * @param message A human readable message - * @param incidentId A random incident id. Should be the same as the logged id for easier error analysis - * @return A response ready to be send to the client - */ - static Response createErrorResponse(int status, String requestMethod, String requestUrl, String errorType, String message, String incidentId) { - return createErrorResponse(Response.status(status).build(), requestMethod, requestUrl, errorType, message, incidentId); - } - - /** - * Create an error response with a human readable yet machine actionable JSON error message, based on an existing - * response (necessary for things like a redirect). - * @param fromResponse The original response to enhance - * @param requestMethod The HTTP method used in the original request - * @param requestUrl The HTTP URL used in the original request - * @param errorType Ideally a exception class name, but maybe free text - * @param message A human readable message - * @param incidentId A random incident id. Should be the same as the logged id for easier error analysis - * @return A response ready to be send to the client - */ - static Response createErrorResponse(Response fromResponse, String requestMethod, String requestUrl, String errorType, String message, String incidentId) { - return Response.fromResponse(fromResponse) - .entity(Json.createObjectBuilder() - .add("status", "ERROR") - .add("code", fromResponse.getStatus()) - .add("method", requestMethod) - .add("url", requestUrl) - .add("errorType", errorType) - .add("message", message) - .add("incidentId", incidentId) - .build() - ) - .type(MediaType.APPLICATION_JSON_TYPE) - .build(); - } - - /** - * Build a complete request URL for logging purposes - * @param req The request - * @return The requests URL sent by the client - */ - static String getOriginalURL(HttpServletRequest req) { - // Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408 - String scheme = req.getScheme(); // http - String serverName = req.getServerName(); // hostname.com - int serverPort = req.getServerPort(); // 80 - String contextPath = req.getContextPath(); // /mywebapp - String servletPath = req.getServletPath(); // /servlet/MyServlet - String pathInfo = req.getPathInfo(); // /a/b;c=123 - String queryString = req.getQueryString(); // d=789 - - // Reconstruct original requesting URL - StringBuilder url = new StringBuilder(); - url.append(scheme).append("://").append(serverName); - if (serverPort != 80 && serverPort != 443) { - url.append(":").append(serverPort); - } - url.append(contextPath).append(servletPath); - if (pathInfo != null) { - url.append(pathInfo); - } - if (queryString != null) { - // filter for unblock-key parameter and mask the key - String maskedQueryString = queryString.replaceAll(ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=.+?\\b", ApiBlockingFilter.UNBLOCK_KEY_QUERYPARAM+"=****"); - url.append("?").append(maskedQueryString); - } - - return url.toString(); + return JSONResponseBuilder.error(Response.Status.INTERNAL_SERVER_ERROR) + .randomIncidentId() + .internalError(ex) + .request(request) + .log(logger, Level.SEVERE, Optional.of(ex)) + .build(); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java index 83b6aaaa33c..4cb88bf2c7a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java @@ -5,14 +5,18 @@ */ package edu.harvard.iq.dataverse.api.errorhandlers; +import edu.harvard.iq.dataverse.api.util.JSONResponseBuilder; import edu.harvard.iq.dataverse.util.BundleUtil; import javax.servlet.http.HttpServletRequest; +import javax.swing.text.html.Option; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; +import java.util.Optional; +import java.util.logging.Level; import java.util.logging.Logger; /** @@ -29,83 +33,72 @@ public class WebApplicationExceptionHandler implements ExceptionMapper Date: Wed, 29 Jul 2020 18:31:16 +0200 Subject: [PATCH 10/11] Add docs about API development and exceptiond handling. #7134 --- doc/sphinx-guides/source/developers/api.rst | 115 ++++++++++++++++++ doc/sphinx-guides/source/developers/index.rst | 1 + 2 files changed, 116 insertions(+) create mode 100644 doc/sphinx-guides/source/developers/api.rst diff --git a/doc/sphinx-guides/source/developers/api.rst b/doc/sphinx-guides/source/developers/api.rst new file mode 100644 index 00000000000..ad7edf00ddd --- /dev/null +++ b/doc/sphinx-guides/source/developers/api.rst @@ -0,0 +1,115 @@ +API development +=============== + +All features of Dataverse are mostly "API too", if not "API first". Making APIs great again helps building integrations +for Dataverse with third party systems, automation and many other use cases. + +Dataverse API is based on `Jersey `_ using JSON all over for both requests and +responses. + +Response Creation +----------------- + +To provide a consistent experience for developers, integrators and users, it's important to streamline what is sent +over the wire as responses. + +.. warning:: To be extended and changed with refactoring of API code to align code paths. + +`JSONResponseBuilder` +^^^^^^^^^^^^^^^^^^^^^ + +To make response building easier and aligned throught the API code base, a factory and decorator pattern based helper +utility has been introduced in `edu.harvard.iq.dataverse.api.util.JSONResponseBuilder`_. + +*Hint: Right now, only the exception handlers listed below use it.* + +Create a response builder by using ``JSONResponseBuilder.error()``. + +1. Add decorations as you see fit (messages, incident identifiers, ...). For current options, see the classes extensive Javadoc. +2. You can also use the same builder to log your response for the admins by using ``.log()`` before you ``.build()``. +3. When writing a filter for the servlet container, you may use ``.apply()`` to sent a response looking and feeling like + when used from a JAX-RS endpoint method. + +.. code-block:: java + :caption: A full fledged example (copied from the ``ThrowableHandler`` described below) + + // Create an instance (contains a human friendly message by default, but may be overridden) + return JSONResponseBuilder.error(Response.Status.INTERNAL_SERVER_ERROR) + // Add an identifier, so admins can find exceptions easier in the logs when users report + .randomIncidentId() + // Hint what went wrong internally by adding exception name & cause + .internalError(ex) + // Add the complete URL & HTTP method the client tried to use + .request(request) + // Write all this to the logs, including the stack trace from the exception + .log(logger, Level.SEVERE, Optional.of(ex)) + // Build the JAX-RS response. Done! + .build(); + +Exception Handling +------------------ + +We have to differ between three types of exceptions thrown during API usage: + +1. Servlet exceptions, based on `javax.servlet.ServletException `_ +2. JAX-RS-native exceptions, based on `javax.ws.rs.WebApplicationException `_ +3. Any other Java exception + +When accessing the API, requests are handled by the servlet container before handing over to Jersey and from there to +the lower layers of bean validation, database connections and all other stuff you might use through the API: + +.. graphviz:: + + digraph G { + rankdir = LR + + node [shape=plain] Client; + node [shape=egg] Network; + + subgraph cluster_0 { + node [shape=box,label="Filters, ..."] F; + node [shape=box,label="JAX-RS/Jersey"] J; + node [shape=box,label="Services/Persistence/..."] D; + + label = "Servlet Container"; + color = black; + } + Client -> Network -> F -> J -> D; + } + +Exceptions before handing over to JAX-RS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The servlet container usually offers limited options to handle errors in elegant ways for an API, which is why you want +to avoid throwing exceptions there. However, it is possible: `see examples by Eugen Baeldung `_ + +You better avoid throwing exceptions from places like +`Filter `_\ s or similar, as those +require specialized code handling instead of going with the below JAX-RS exception handling. + +Exceptions after handing over to JAX-RS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Cases 2) and 3) above are both happening in code easy to control in API code without fiddling with ``web.xml`` or similar. + +Easy to follow code examples and explanations: + +- https://www.baeldung.com/jersey-rest-api-with-spring#using-exceptionmapper (never mind this is about Spring, the technology is the same!) +- https://www.baeldung.com/jersey-bean-validation#custom-exception-handler + +For Dataverse, all API exception handlers live in package `edu.harvard.iq.dataverse.api.errorhandlers`_. +*Remember, this does not handle exceptions popping via web UI, thus not served by Jersey JAX-RS!* + +1. ``ThrowableHandler`` catches any Java exception thrown and uncatched in business logic (either by accident or on purpose) + or not having a more specialized ``ExceptionMapper``. It allows for handing out nice JSON error messages to users and + detailed logging for admins. +2. ``WebApplicationExceptionHandler`` catches all JAX-RS typed exceptions, which usually depict HTTP statuses. + Those "native" exceptions are commonly used for redirection, client and server errors, so better watch out what you + catch and what you do when the fish bit. +3. ``ConstraintViolationExceptionHandler`` allows for catching and formating bean validation exception from any + layer of validation (JAX-RS itself, persistence, ...) +4. ``JsonParseExceptionHandler`` catches and formats error messages when a user or admin send an invalid JSON document + to and endpoint and validation failed. (Usually contains hints about what is wrong...) + +.. _edu.harvard.iq.dataverse.api.errorhandlers: https://github.com/IQSS/dataverse/tree/develop/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers +.. _edu.harvard.iq.dataverse.api.util.JSONResponseBuilder: https://github.com/IQSS/dataverse/tree/develop/src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java \ No newline at end of file diff --git a/doc/sphinx-guides/source/developers/index.rst b/doc/sphinx-guides/source/developers/index.rst index 96595220e07..1a38206d1bb 100755 --- a/doc/sphinx-guides/source/developers/index.rst +++ b/doc/sphinx-guides/source/developers/index.rst @@ -17,6 +17,7 @@ Developer Guide troubleshooting version-control sql-upgrade-scripts + api testing documentation dependencies From a08b290b4bbd531973efa5dcfa1086be0e81b4a9 Mon Sep 17 00:00:00 2001 From: Oliver Bertuch Date: Wed, 29 Jul 2020 18:45:05 +0200 Subject: [PATCH 11/11] Rename JSONResponseBuilder into more CamelCased JsonResponseBuilder. #7134 --- doc/sphinx-guides/source/developers/api.rst | 6 ++-- .../api/errorhandlers/ThrowableHandler.java | 8 ++--- .../WebApplicationExceptionHandler.java | 5 ++- ...eBuilder.java => JsonResponseBuilder.java} | 32 +++++++++---------- .../api/util/JSONResponseBuilderTest.java | 3 +- 5 files changed, 24 insertions(+), 30 deletions(-) rename src/main/java/edu/harvard/iq/dataverse/api/util/{JSONResponseBuilder.java => JsonResponseBuilder.java} (91%) diff --git a/doc/sphinx-guides/source/developers/api.rst b/doc/sphinx-guides/source/developers/api.rst index ad7edf00ddd..c1a503e7f60 100644 --- a/doc/sphinx-guides/source/developers/api.rst +++ b/doc/sphinx-guides/source/developers/api.rst @@ -15,15 +15,15 @@ over the wire as responses. .. warning:: To be extended and changed with refactoring of API code to align code paths. -`JSONResponseBuilder` +`JsonResponseBuilder` ^^^^^^^^^^^^^^^^^^^^^ To make response building easier and aligned throught the API code base, a factory and decorator pattern based helper -utility has been introduced in `edu.harvard.iq.dataverse.api.util.JSONResponseBuilder`_. +utility has been introduced in `edu.harvard.iq.dataverse.api.util.JsonResponseBuilder`_. *Hint: Right now, only the exception handlers listed below use it.* -Create a response builder by using ``JSONResponseBuilder.error()``. +Create a response builder by using ``JsonResponseBuilder.error()``. 1. Add decorations as you see fit (messages, incident identifiers, ...). For current options, see the classes extensive Javadoc. 2. You can also use the same builder to log your response for the admins by using ``.log()`` before you ``.build()``. diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java index 74330c04c4d..7b7f1f08fad 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/ThrowableHandler.java @@ -1,19 +1,15 @@ package edu.harvard.iq.dataverse.api.errorhandlers; -import edu.harvard.iq.dataverse.api.util.JSONResponseBuilder; +import edu.harvard.iq.dataverse.api.util.JsonResponseBuilder; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; -import java.util.Map; import java.util.Optional; -import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Produces a generic 500 message for the API, being a fallback handler for not specially treated exceptions. @@ -31,7 +27,7 @@ public class ThrowableHandler implements ExceptionMapper{ @Override public Response toResponse(Throwable ex){ - return JSONResponseBuilder.error(Response.Status.INTERNAL_SERVER_ERROR) + return JsonResponseBuilder.error(Response.Status.INTERNAL_SERVER_ERROR) .randomIncidentId() .internalError(ex) .request(request) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java index 4cb88bf2c7a..b6229e58192 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/errorhandlers/WebApplicationExceptionHandler.java @@ -5,11 +5,10 @@ */ package edu.harvard.iq.dataverse.api.errorhandlers; -import edu.harvard.iq.dataverse.api.util.JSONResponseBuilder; +import edu.harvard.iq.dataverse.api.util.JsonResponseBuilder; import edu.harvard.iq.dataverse.util.BundleUtil; import javax.servlet.http.HttpServletRequest; -import javax.swing.text.html.Option; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; @@ -38,7 +37,7 @@ public Response toResponse(WebApplicationException ex) { return ex.getResponse(); // Otherwise, do stuff. - JSONResponseBuilder jrb = JSONResponseBuilder.error(ex.getResponse()); + JsonResponseBuilder jrb = JsonResponseBuilder.error(ex.getResponse()); // See also https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for a list of status codes. switch (ex.getResponse().getStatus()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java similarity index 91% rename from src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java rename to src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java index eea46eada2e..1cc9e8d77f5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilder.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/util/JsonResponseBuilder.java @@ -15,13 +15,13 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class JSONResponseBuilder { +public class JsonResponseBuilder { private JsonObjectBuilder entityBuilder = Json.createObjectBuilder(); private Response.ResponseBuilder jerseyResponseBuilder; private boolean alreadyLogged = false; - private JSONResponseBuilder() {} + private JsonResponseBuilder() {} /** * Create an error response from an numeric error code (should be >= 400) @@ -29,12 +29,12 @@ private JSONResponseBuilder() {} * @return A builder with a basic JSON body * @throws IllegalArgumentException if fromResponse isn't an error response */ - public static JSONResponseBuilder error(int httpStatusCode) { + public static JsonResponseBuilder error(int httpStatusCode) { if (httpStatusCode < 400) { throw new IllegalArgumentException("A status code < 400 cannot be an error indicating response."); } - JSONResponseBuilder b = new JSONResponseBuilder(); + JsonResponseBuilder b = new JsonResponseBuilder(); b.jerseyResponseBuilder = Response.status(httpStatusCode); b.entityBuilder.add("status", "ERROR"); b.entityBuilder.add("code", httpStatusCode); @@ -50,8 +50,8 @@ public static JSONResponseBuilder error(int httpStatusCode) { * @return A builder with a basic JSON body * @throws IllegalArgumentException if fromResponse isn't an error response */ - public static JSONResponseBuilder error(Response.Status status) { - JSONResponseBuilder b = error(status.getStatusCode()); + public static JsonResponseBuilder error(Response.Status status) { + JsonResponseBuilder b = error(status.getStatusCode()); b.jerseyResponseBuilder = Response.status(status); return b; } @@ -62,8 +62,8 @@ public static JSONResponseBuilder error(Response.Status status) { * @return A builder with a basic JSON body * @throws IllegalArgumentException if fromResponse isn't an error response */ - public static JSONResponseBuilder error(Response fromResponse) { - JSONResponseBuilder b = error(fromResponse.getStatus()); + public static JsonResponseBuilder error(Response fromResponse) { + JsonResponseBuilder b = error(fromResponse.getStatus()); b.jerseyResponseBuilder = Response.fromResponse(fromResponse); return b; } @@ -73,7 +73,7 @@ public static JSONResponseBuilder error(Response fromResponse) { * @param message A human readable message * @return The enhanced builder */ - public JSONResponseBuilder message(String message) { + public JsonResponseBuilder message(String message) { this.entityBuilder.add("message", message); return this; } @@ -83,7 +83,7 @@ public JSONResponseBuilder message(String message) { * @param id A String containing an (ideally unique) identifier * @return The enhanced builder */ - public JSONResponseBuilder incidentId(String id) { + public JsonResponseBuilder incidentId(String id) { this.entityBuilder.add("incidentId", id); return this; } @@ -92,7 +92,7 @@ public JSONResponseBuilder incidentId(String id) { * Add a UUID random identifier for errors. Will overwrite existing. * @return The enhanced builder */ - public JSONResponseBuilder randomIncidentId() { + public JsonResponseBuilder randomIncidentId() { this.entityBuilder.add("incidentId", UUID.randomUUID().toString()); return this; } @@ -103,7 +103,7 @@ public JSONResponseBuilder randomIncidentId() { * @param request The original request (usually provided from a context) * @return The enhanced builder */ - public JSONResponseBuilder request(HttpServletRequest request) { + public JsonResponseBuilder request(HttpServletRequest request) { this.entityBuilder.add("requestUrl", getOriginalURL(request)); this.entityBuilder.add("requestMethod", request.getMethod()); return this; @@ -114,7 +114,7 @@ public JSONResponseBuilder request(HttpServletRequest request) { * @param request The original request (usually provided from a context) * @return The enhanced builder */ - public JSONResponseBuilder requestContentType(HttpServletRequest request) { + public JsonResponseBuilder requestContentType(HttpServletRequest request) { this.entityBuilder.add("requestContentType", request.getContentType()); return this; } @@ -125,7 +125,7 @@ public JSONResponseBuilder requestContentType(HttpServletRequest request) { * @param ex An exception. * @return The enhanced builder */ - public JSONResponseBuilder internalError(Throwable ex) { + public JsonResponseBuilder internalError(Throwable ex) { this.entityBuilder.add("interalError", ex.getClass().getSimpleName()); if (ex.getCause() != null) { this.entityBuilder.add("internalCause", ex.getCause().getClass().getSimpleName()); @@ -181,7 +181,7 @@ public void apply(HttpServletResponse response) throws IOException { * @param level Provide a level at which this should be logged * @return The unmodified builder. */ - public JSONResponseBuilder log(Logger logger, Level level) { + public JsonResponseBuilder log(Logger logger, Level level) { return this.log(logger, level, Optional.empty()); } @@ -202,7 +202,7 @@ public JSONResponseBuilder log(Logger logger, Level level) { * @param ex An optional exception to be included in the log message. * @return The unmodified builder. */ - public JSONResponseBuilder log(Logger logger, Level level, Optional ex) { + public JsonResponseBuilder log(Logger logger, Level level, Optional ex) { if ( ! logger.isLoggable(level) || alreadyLogged ) return this; diff --git a/src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java b/src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java index 29e0536f73c..c9ec80d6d31 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/util/JSONResponseBuilderTest.java @@ -1,7 +1,6 @@ package edu.harvard.iq.dataverse.api.util; import edu.harvard.iq.dataverse.api.ApiBlockingFilter; -import edu.harvard.iq.dataverse.api.errorhandlers.ThrowableHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.NullSource; @@ -27,6 +26,6 @@ class JSONResponseBuilderTest { void testMaskingOriginalURL(String query) { HttpServletRequest test = Mockito.mock(HttpServletRequest.class); when(test.getQueryString()).thenReturn(query); - assertFalse(JSONResponseBuilder.getOriginalURL(test).contains("supersecret")); + assertFalse(JsonResponseBuilder.getOriginalURL(test).contains("supersecret")); } } \ No newline at end of file