From a4b5e4dc69a8d903567a7fa821c454a5b07e3500 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 30 Aug 2018 16:08:49 +0200 Subject: [PATCH 01/23] Update Zalando-problem libraries Update the following libraries: - jackson-datatype-problem to 0.21.0 - problem to 0.21.0 - problem-spring-web to 0.23.0 Code fixes and refactoring of exception handling --- CHANGELOG.md | 1 + build.gradle | 6 +- .../CompressedEventPublishingAT.java | 4 +- .../nakadi/webservice/EventTypeAT.java | 4 +- .../webservice/hila/SubscriptionAT.java | 6 +- .../nakadi/config/SecurityConfiguration.java | 24 +- .../CursorOperationsController.java | 16 +- .../nakadi/controller/CursorsController.java | 53 +- .../controller/EventPublishingController.java | 23 +- .../controller/EventStreamController.java | 24 +- .../controller/EventTypeController.java | 68 +-- .../nakadi/controller/ExceptionHandling.java | 327 ------------ .../controller/HealthCheckController.java | 5 +- .../NakadiProblemControllerAdvice.java | 7 + .../controller/NakadiProblemHandling.java | 473 ++++++++++++++++++ .../controller/PartitionsController.java | 29 +- .../PostSubscriptionController.java | 38 +- .../nakadi/controller/SettingsController.java | 25 +- .../controller/SubscriptionController.java | 56 +-- .../SubscriptionStreamController.java | 26 +- .../controller/TimelinesController.java | 38 +- .../nakadi/controller/VersionController.java | 4 +- .../nakadi/problem/ValidationProblem.java | 18 +- .../nakadi/util/GzipBodyRequestFilter.java | 6 +- .../controller/CursorsControllerTest.java | 6 +- .../controller/EventStreamControllerTest.java | 14 +- .../EventTypeAuthorizationTest.java | 12 +- .../controller/EventTypeControllerTest.java | 23 +- .../EventTypeControllerTestCase.java | 2 +- ...st.java => NakadiProblemHandlingTest.java} | 8 +- .../controller/PartitionsControllerTest.java | 10 +- .../PostSubscriptionControllerTest.java | 4 +- .../SubscriptionControllerTest.java | 11 +- .../controller/TimelinesControllerTest.java | 2 +- .../org/zalando/nakadi/utils/TestUtils.java | 4 +- 35 files changed, 650 insertions(+), 727 deletions(-) delete mode 100644 src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java create mode 100644 src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java create mode 100644 src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java rename src/test/java/org/zalando/nakadi/controller/{ExceptionHandlingTest.java => NakadiProblemHandlingTest.java} (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ea114c5c..18b7fa1eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Upgraded dependencies - Refactored exceptions - Moved Problem creation to controller +- Upgraded Zalando-problem libraries ## [2.8.3] - 2018-08-01 diff --git a/build.gradle b/build.gradle index 0be33a08e8..1ab1cf0632 100644 --- a/build.gradle +++ b/build.gradle @@ -127,9 +127,9 @@ dependencies { compile('org.zalando.stups:stups-spring-oauth2-server:1.0.22') { exclude module: "httpclient" } - compile 'org.zalando:jackson-datatype-problem:0.5.0' - compile 'org.zalando:problem:0.5.0' - compile 'org.zalando:problem-spring-web:0.5.0' + compile 'org.zalando:jackson-datatype-problem:0.21.0' + compile 'org.zalando:problem:0.21.0' + compile 'org.zalando:problem-spring-web:0.23.0' compile 'com.google.guava:guava:25.1-jre' compile 'org.slf4j:slf4j-log4j12' compile "io.dropwizard.metrics:metrics-core:$dropwizardVersion" diff --git a/src/acceptance-test/java/org/zalando/nakadi/webservice/CompressedEventPublishingAT.java b/src/acceptance-test/java/org/zalando/nakadi/webservice/CompressedEventPublishingAT.java index 582a0d2e22..e77eb01a56 100644 --- a/src/acceptance-test/java/org/zalando/nakadi/webservice/CompressedEventPublishingAT.java +++ b/src/acceptance-test/java/org/zalando/nakadi/webservice/CompressedEventPublishingAT.java @@ -18,8 +18,8 @@ import static com.jayway.restassured.RestAssured.given; import static com.jayway.restassured.http.ContentType.JSON; import static java.text.MessageFormat.format; -import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING; -import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; +import static org.springframework.http.HttpHeaders.CONTENT_ENCODING; +import static org.zalando.problem.Status.NOT_ACCEPTABLE; public class CompressedEventPublishingAT extends BaseAT { diff --git a/src/acceptance-test/java/org/zalando/nakadi/webservice/EventTypeAT.java b/src/acceptance-test/java/org/zalando/nakadi/webservice/EventTypeAT.java index 0ba764fb80..925749e044 100644 --- a/src/acceptance-test/java/org/zalando/nakadi/webservice/EventTypeAT.java +++ b/src/acceptance-test/java/org/zalando/nakadi/webservice/EventTypeAT.java @@ -23,7 +23,6 @@ import org.zalando.nakadi.repository.kafka.KafkaTestHelper; import org.zalando.nakadi.utils.EventTypeTestBuilder; import org.zalando.nakadi.webservice.utils.NakadiTestUtils; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; import java.io.IOException; @@ -49,6 +48,7 @@ import static org.zalando.nakadi.utils.TestUtils.resourceAsString; import static org.zalando.nakadi.utils.TestUtils.waitFor; import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.publishEvent; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class EventTypeAT extends BaseAT { @@ -346,7 +346,7 @@ public void whenUpdateETAuthObjectThen422() throws Exception { .put("/event-types/" + eventType.getName()) .then() .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) - .body(equalTo(MAPPER.writer().writeValueAsString(Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, + .body(equalTo(MAPPER.writer().writeValueAsString(Problem.valueOf(UNPROCESSABLE_ENTITY, "Changing authorization object to `null` is not possible due to existing one")))); } diff --git a/src/acceptance-test/java/org/zalando/nakadi/webservice/hila/SubscriptionAT.java b/src/acceptance-test/java/org/zalando/nakadi/webservice/hila/SubscriptionAT.java index 18283d60bb..f49bbe751e 100644 --- a/src/acceptance-test/java/org/zalando/nakadi/webservice/hila/SubscriptionAT.java +++ b/src/acceptance-test/java/org/zalando/nakadi/webservice/hila/SubscriptionAT.java @@ -32,7 +32,6 @@ import org.zalando.nakadi.webservice.utils.NakadiTestUtils; import org.zalando.nakadi.webservice.utils.TestStreamingClient; import org.zalando.nakadi.webservice.utils.ZookeeperTestUtils; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; import java.io.IOException; @@ -63,6 +62,7 @@ import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.publishBusinessEventWithUserDefinedPartition; import static org.zalando.nakadi.webservice.utils.NakadiTestUtils.publishEvents; import static org.zalando.nakadi.webservice.utils.TestStreamingClient.SESSION_ID_UNKNOWN; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class SubscriptionAT extends BaseAT { @@ -460,7 +460,7 @@ public void whenStreamDuplicatePartitionsThenUnprocessableEntity() throws IOExce .then() .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) .body(JSON_HELPER.matchesObject(Problem.valueOf( - MoreStatus.UNPROCESSABLE_ENTITY, + UNPROCESSABLE_ENTITY, "Duplicated partition specified"))); } @@ -480,7 +480,7 @@ public void whenStreamWrongPartitionsThenUnprocessableEntity() throws IOExceptio .then() .statusCode(HttpStatus.SC_UNPROCESSABLE_ENTITY) .body(JSON_HELPER.matchesObject(Problem.valueOf( - MoreStatus.UNPROCESSABLE_ENTITY, + UNPROCESSABLE_ENTITY, "Wrong partitions specified - some partitions don't belong to subscription: " + "EventTypePartition{eventType='" + et + "', partition='1'}, " + "EventTypePartition{eventType='dummy-et-123', partition='0'}"))); diff --git a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java index 81b10df382..223843af08 100644 --- a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java +++ b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java @@ -24,10 +24,11 @@ import org.springframework.security.web.firewall.HttpFirewall; import org.springframework.security.web.firewall.RequestRejectedException; import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.zalando.problem.Status; +import org.zalando.problem.StatusType; import org.zalando.stups.oauth2.spring.security.expression.ExtendedOAuth2WebSecurityExpressionHandler; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Response; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -41,6 +42,8 @@ import static org.springframework.http.HttpMethod.GET; import static org.springframework.http.HttpMethod.POST; import static org.springframework.http.HttpMethod.PUT; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.UNAUTHORIZED; @EnableResourceServer @Configuration @@ -156,15 +159,15 @@ protected Object toJsonResponse(final Object object) { final OAuth2Exception oae = (OAuth2Exception) object; if (oae.getCause() != null) { if (oae.getCause() instanceof AuthenticationException) { - return new ProblemResponse(Response.Status.UNAUTHORIZED, oae.getCause().getMessage()); + return new ProblemResponse(UNAUTHORIZED, oae.getCause().getMessage()); } - return new ProblemResponse(Response.Status.INTERNAL_SERVER_ERROR, oae.getMessage()); + return new ProblemResponse(INTERNAL_SERVER_ERROR, oae.getMessage()); } - return new ProblemResponse(Response.Status.fromStatusCode(oae.getHttpErrorCode()), oae.getMessage()); + return new ProblemResponse(fromStatusCode(oae.getHttpErrorCode()), oae.getMessage()); } - return new ProblemResponse(Response.Status.INTERNAL_SERVER_ERROR, + return new ProblemResponse(INTERNAL_SERVER_ERROR, "Unrecognized error happened in authentication path"); } } @@ -175,7 +178,7 @@ private static class ProblemResponse { private final int status; private final String detail; - ProblemResponse(final Response.StatusType status, final String detail) { + ProblemResponse(final StatusType status, final String detail) { this.type = "https://httpstatus.es/" + status.getStatusCode(); this.title = status.getReasonPhrase(); this.status = status.getStatusCode(); @@ -336,4 +339,13 @@ private void rejectedBlacklistedUrls(final HttpServletRequest request) { } + private static Status fromStatusCode(final int code) { + for (final Status status: Status.values()) { + if (status.getStatusCode() == code) { + return status; + } + } + return null; + } + } diff --git a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java index ab640408df..f5218c4d77 100644 --- a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java +++ b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java @@ -32,9 +32,7 @@ import org.zalando.nakadi.view.CursorDistance; import org.zalando.nakadi.view.CursorLag; import org.zalando.nakadi.view.ShiftedCursor; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.validation.Valid; import java.util.List; @@ -43,9 +41,10 @@ import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.ResponseEntity.status; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class CursorOperationsController { +public class CursorOperationsController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(CursorOperationsController.class); @@ -132,12 +131,13 @@ public List cursorsLag(@PathVariable("eventTypeName") final String ev .collect(Collectors.toList()); } + @Override @ExceptionHandler(InvalidCursorOperation.class) - public ResponseEntity invalidCursorOperation(final InvalidCursorOperation e, - final NativeWebRequest request) { - LOG.debug("User provided invalid cursor for operation. Reason: " + e.getReason(), e); - return Responses.create(Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, - clientErrorMessage(e.getReason())), request); + public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, + final NativeWebRequest request) { + LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, + clientErrorMessage(exception.getReason())), request); } private String clientErrorMessage(final InvalidCursorOperation.Reason reason) { diff --git a/src/main/java/org/zalando/nakadi/controller/CursorsController.java b/src/main/java/org/zalando/nakadi/controller/CursorsController.java index 941f5ab5de..b375482dd4 100644 --- a/src/main/java/org/zalando/nakadi/controller/CursorsController.java +++ b/src/main/java/org/zalando/nakadi/controller/CursorsController.java @@ -15,15 +15,12 @@ import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.NakadiCursor; -import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; import org.zalando.nakadi.exceptions.runtime.CursorsAreEmptyException; -import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidCursorException; -import org.zalando.nakadi.exceptions.runtime.InvalidStreamIdException; +import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; -import org.zalando.nakadi.exceptions.runtime.RequestInProgressException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; import org.zalando.nakadi.exceptions.runtime.UnableProcessException; import org.zalando.nakadi.problem.ValidationProblem; @@ -34,28 +31,24 @@ import org.zalando.nakadi.view.CursorCommitResult; import org.zalando.nakadi.view.SubscriptionCursor; import org.zalando.nakadi.view.SubscriptionCursorWithoutToken; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.validation.Valid; import javax.validation.constraints.NotNull; -import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.springframework.http.ResponseEntity.noContent; import static org.springframework.http.ResponseEntity.ok; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; -import static org.zalando.problem.spring.web.advice.Responses.create; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class CursorsController { +public class CursorsController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(CursorsController.class); @@ -152,39 +145,19 @@ private List convertToNakadiCursors( return nakadiCursors; } - @ExceptionHandler(InvalidStreamIdException.class) - public ResponseEntity handleInvalidStreamId(final InvalidStreamIdException ex, - final NativeWebRequest request) { - LOG.warn("Stream id {} is not found: {}", ex.getStreamId(), ex.getMessage()); - return Responses.create(MoreStatus.UNPROCESSABLE_ENTITY, ex.getMessage(), request); - } - + @Override @ExceptionHandler(UnableProcessException.class) - public ResponseEntity handleUnableProcessException(final RuntimeException ex, + public ResponseEntity handleUnableProcessException(final UnableProcessException ex, final NativeWebRequest request) { LOG.debug(ex.getMessage(), ex); - return Responses.create(SERVICE_UNAVAILABLE, ex.getMessage(), request); - } - - @ExceptionHandler(RequestInProgressException.class) - public ResponseEntity handleRequestInProgressException(final RequestInProgressException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(Response.Status.CONFLICT, ex.getMessage(), request); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, ex.getMessage()), request); } + @Override @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException(final MethodArgumentNotValidException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(new ValidationProblem(ex.getBindingResult()), request); - } - - @ExceptionHandler(FeatureNotAvailableException.class) - public ResponseEntity handleFeatureNotAllowed(final FeatureNotAvailableException ex, - final NativeWebRequest request) { + public ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, + final NativeWebRequest request) { LOG.debug(ex.getMessage(), ex); - return Responses.create(Problem.valueOf(Response.Status.NOT_IMPLEMENTED, "Feature is disabled"), request); + return create(new ValidationProblem(ex.getBindingResult()), request); } - } diff --git a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java index f74b47c35d..160585864c 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java @@ -29,21 +29,20 @@ import org.zalando.nakadi.service.NakadiKpiPublisher; import org.zalando.problem.Problem; import org.zalando.problem.ThrowableProblem; -import org.zalando.problem.spring.web.advice.Responses; -import javax.ws.rs.core.Response; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.springframework.http.ResponseEntity.status; import static org.springframework.web.bind.annotation.RequestMethod.POST; -import static org.zalando.problem.spring.web.advice.Responses.create; +import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; @RestController -public class EventPublishingController { +public class EventPublishingController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(EventPublishingController.class); @@ -77,8 +76,8 @@ public ResponseEntity postEvent(@PathVariable final String eventTypeName, try { if (blacklistService.isProductionBlocked(eventTypeName, client.getClientId())) { - return Responses.create( - Problem.valueOf(Response.Status.FORBIDDEN, "Application or event type is blocked"), request); + return create( + Problem.valueOf(FORBIDDEN, "Application or event type is blocked"), request); } final ResponseEntity response = postEventInternal( @@ -86,7 +85,7 @@ public ResponseEntity postEvent(@PathVariable final String eventTypeName, eventTypeMetrics.incrementResponseCount(response.getStatusCode().value()); return response; } catch (final RuntimeException ex) { - eventTypeMetrics.incrementResponseCount(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + eventTypeMetrics.incrementResponseCount(INTERNAL_SERVER_ERROR.getStatusCode()); throw ex; } } @@ -162,11 +161,11 @@ private ResponseEntity processJSONException(final JSONException e, final NativeW if (e.getCause() == null) { return create(createProblem(e), nativeWebRequest); } - return create(Problem.valueOf(Response.Status.BAD_REQUEST), nativeWebRequest); + return create(Problem.valueOf(BAD_REQUEST), nativeWebRequest); } private ThrowableProblem createProblem(final JSONException e) { - return Problem.valueOf(Response.Status.BAD_REQUEST, "Error occurred when parsing event(s). " + e.getMessage()); + return Problem.valueOf(BAD_REQUEST, "Error occurred when parsing event(s). " + e.getMessage()); } private ResponseEntity response(final EventPublishResult result) { diff --git a/src/main/java/org/zalando/nakadi/controller/EventStreamController.java b/src/main/java/org/zalando/nakadi/controller/EventStreamController.java index d6a441d35a..97d5ecd479 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventStreamController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventStreamController.java @@ -49,13 +49,12 @@ import org.zalando.nakadi.service.timeline.TimelineService; import org.zalando.nakadi.util.FlowIdUtils; import org.zalando.nakadi.view.Cursor; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; +import org.zalando.problem.StatusType; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.Response; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; @@ -67,14 +66,15 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; -import static javax.ws.rs.core.Response.Status.FORBIDDEN; -import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.zalando.nakadi.metrics.MetricUtils.metricNameFor; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; +import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.PRECONDITION_FAILED; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.TOO_MANY_REQUESTS; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController public class EventStreamController { @@ -202,7 +202,7 @@ public StreamingResponseBody streamEvents( if (blacklistService.isConsumptionBlocked(eventTypeName, client.getClientId())) { writeProblemResponse(response, outputStream, - Problem.valueOf(Response.Status.FORBIDDEN, "Application or event type is blocked")); + Problem.valueOf(FORBIDDEN, "Application or event type is blocked")); return; } @@ -269,7 +269,7 @@ public StreamingResponseBody streamEvents( } catch (final NoConnectionSlotsException e) { LOG.debug("Connection creation failed due to exceeding max connection count"); writeProblemResponse(response, outputStream, - Problem.valueOf(MoreStatus.TOO_MANY_REQUESTS, e.getMessage())); + Problem.valueOf(TOO_MANY_REQUESTS, e.getMessage())); } catch (final ServiceTemporarilyUnavailableException e) { LOG.error("Error while trying to stream events.", e); writeProblemResponse(response, outputStream, SERVICE_UNAVAILABLE, e.getMessage()); @@ -314,7 +314,7 @@ private String getKafkaQuotaClientId(final String eventTypeName, final Client cl } private void writeProblemResponse(final HttpServletResponse response, final OutputStream outputStream, - final Response.StatusType statusCode, final String message) throws IOException { + final StatusType statusCode, final String message) throws IOException { writeProblemResponse(response, outputStream, Problem.valueOf(statusCode, message)); } diff --git a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java index 306513274e..304541254c 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java @@ -8,7 +8,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.Errors; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,8 +22,6 @@ import org.zalando.nakadi.exceptions.runtime.ConflictException; import org.zalando.nakadi.exceptions.runtime.DuplicatedEventTypeNameException; import org.zalando.nakadi.exceptions.runtime.EventTypeDeletionException; -import org.zalando.nakadi.exceptions.runtime.EventTypeOptionsValidationException; -import org.zalando.nakadi.exceptions.runtime.EventTypeUnavailableException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidEventTypeException; @@ -41,22 +38,18 @@ import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.EventTypeService; import org.zalando.nakadi.service.FeatureToggleService; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.validation.Valid; -import javax.ws.rs.core.Response; import java.util.List; import java.util.stream.Collectors; import static org.springframework.http.ResponseEntity.status; import static org.zalando.nakadi.service.FeatureToggleService.Feature.DISABLE_EVENT_TYPE_CREATION; import static org.zalando.nakadi.service.FeatureToggleService.Feature.DISABLE_EVENT_TYPE_DELETION; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; @RestController @RequestMapping(value = "/event-types") -public class EventTypeController { +public class EventTypeController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(TimelinesController.class); @@ -97,7 +90,7 @@ public ResponseEntity create(@Valid @RequestBody final EventTypeBase eventTyp } if (errors.hasErrors()) { - return Responses.create(new ValidationProblem(errors), request); + return create(new ValidationProblem(errors), request); } eventTypeService.create(eventType); @@ -135,7 +128,7 @@ public ResponseEntity update( UnableProcessException, NoSuchPartitionStrategyException{ if (errors.hasErrors()) { - return Responses.create(new ValidationProblem(errors), request); + return create(new ValidationProblem(errors), request); } eventTypeService.update(name, eventType); @@ -168,59 +161,4 @@ private HttpHeaders generateWarningHeaders(final EventTypeBase eventType) { return headers; } - - @ExceptionHandler(EventTypeDeletionException.class) - public ResponseEntity deletion(final EventTypeDeletionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Response.Status.INTERNAL_SERVER_ERROR, exception.getMessage(), request); - } - - @ExceptionHandler(UnableProcessException.class) - public ResponseEntity unableProcess(final UnableProcessException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(ConflictException.class) - public ResponseEntity conflict(final ConflictException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Response.Status.CONFLICT, exception.getMessage(), request); - } - - @ExceptionHandler(EventTypeUnavailableException.class) - public ResponseEntity eventTypeUnavailable(final EventTypeUnavailableException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, exception.getMessage(), request); - } - - @ExceptionHandler(NoSuchPartitionStrategyException.class) - public ResponseEntity noSuchPartitionStrategyException(final NoSuchPartitionStrategyException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(DuplicatedEventTypeNameException.class) - public ResponseEntity duplicatedEventTypeNameException(final DuplicatedEventTypeNameException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Problem.valueOf(Response.Status.CONFLICT, exception.getMessage()), request); - } - - @ExceptionHandler(InvalidEventTypeException.class) - public ResponseEntity invalidEventTypeException(final InvalidEventTypeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(EventTypeOptionsValidationException.class) - public ResponseEntity unableProcess(final EventTypeOptionsValidationException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java b/src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java deleted file mode 100644 index f47f8d3cd7..0000000000 --- a/src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java +++ /dev/null @@ -1,327 +0,0 @@ -package org.zalando.nakadi.controller; - -import com.fasterxml.jackson.databind.JsonMappingException; -import com.google.common.base.CaseFormat; -import org.apache.commons.lang3.RandomStringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.NativeWebRequest; -import org.zalando.nakadi.exceptions.runtime.AccessDeniedException; -import org.zalando.nakadi.exceptions.runtime.CompactionException; -import org.zalando.nakadi.exceptions.runtime.CursorConversionException; -import org.zalando.nakadi.exceptions.runtime.CursorsAreEmptyException; -import org.zalando.nakadi.exceptions.runtime.DbWriteOperationsBlockedException; -import org.zalando.nakadi.exceptions.runtime.DuplicatedStorageException; -import org.zalando.nakadi.exceptions.runtime.EnrichmentException; -import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; -import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; -import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; -import org.zalando.nakadi.exceptions.runtime.InvalidLimitException; -import org.zalando.nakadi.exceptions.runtime.InvalidPartitionKeyFieldsException; -import org.zalando.nakadi.exceptions.runtime.InvalidVersionNumberException; -import org.zalando.nakadi.exceptions.runtime.LimitReachedException; -import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; -import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; -import org.zalando.nakadi.exceptions.runtime.NoStreamingSlotsAvailable; -import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; -import org.zalando.nakadi.exceptions.runtime.NoSuchPartitionStrategyException; -import org.zalando.nakadi.exceptions.runtime.NoSuchSchemaException; -import org.zalando.nakadi.exceptions.runtime.NoSuchStorageException; -import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; -import org.zalando.nakadi.exceptions.runtime.PartitioningException; -import org.zalando.nakadi.exceptions.runtime.RepositoryProblemException; -import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; -import org.zalando.nakadi.exceptions.runtime.StorageIsUsedException; -import org.zalando.nakadi.exceptions.runtime.TimelineException; -import org.zalando.nakadi.exceptions.runtime.TopicCreationException; -import org.zalando.nakadi.exceptions.runtime.UnknownStorageTypeException; -import org.zalando.nakadi.exceptions.runtime.UnprocessableEntityException; -import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; -import org.zalando.problem.MoreStatus; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.ProblemHandling; -import org.zalando.problem.spring.web.advice.Responses; - -import javax.ws.rs.core.Response; - -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; -import static javax.ws.rs.core.Response.Status.CONFLICT; -import static javax.ws.rs.core.Response.Status.FORBIDDEN; -import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; - - -@ControllerAdvice -public final class ExceptionHandling implements ProblemHandling { - - private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandling.class); - - @Override - public String formatFieldName(final String fieldName) { - return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName); - } - - @Override - @ExceptionHandler - public ResponseEntity handleThrowable(final Throwable throwable, final NativeWebRequest request) { - final String errorTraceId = generateErrorTraceId(); - LOG.error("InternalServerError (" + errorTraceId + "):", throwable); - return Responses.create(Response.Status.INTERNAL_SERVER_ERROR, "An internal error happened. Please report it. (" - + errorTraceId + ")", request); - } - - private String generateErrorTraceId() { - return "ETI" + RandomStringUtils.randomAlphanumeric(24); - } - - @Override - @ExceptionHandler - public ResponseEntity handleMessageNotReadableException(final HttpMessageNotReadableException exception, - final NativeWebRequest request) { - /* - Unwrap nested JsonMappingException because the enclosing HttpMessageNotReadableException adds some ugly, Java - class and stacktrace like information. - */ - final Throwable mostSpecificCause = exception.getMostSpecificCause(); - final String message; - if (mostSpecificCause instanceof JsonMappingException) { - message = mostSpecificCause.getMessage(); - } else { - message = exception.getMessage(); - } - return Responses.create(Response.Status.BAD_REQUEST, message, request); - } - - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity accessDeniedException(final AccessDeniedException exception, - final NativeWebRequest request) { - return Responses.create(Response.Status.FORBIDDEN, exception.explain(), request); - } - - @ExceptionHandler(IllegalClientIdException.class) - public ResponseEntity handleIllegalClientIdException(final IllegalClientIdException exception, - final NativeWebRequest request) { - return Responses.create(Response.Status.FORBIDDEN, exception.getMessage(), request); - } - - @ExceptionHandler(CursorsAreEmptyException.class) - public ResponseEntity handleCursorsUnavailableException(final RuntimeException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(MoreStatus.UNPROCESSABLE_ENTITY, ex.getMessage(), request); - } - - @ExceptionHandler - public ResponseEntity handleExceptionWrapper(final NakadiRuntimeException exception, - final NativeWebRequest request) throws Exception { - final Throwable cause = exception.getCause(); - if (cause instanceof InternalNakadiException) { - return Responses.create(INTERNAL_SERVER_ERROR, exception.getMessage(), request); - } - throw exception.getException(); - } - - @ExceptionHandler(RepositoryProblemException.class) - public ResponseEntity handleRepositoryProblem(final RepositoryProblemException exception, - final NativeWebRequest request) { - LOG.error("Repository problem occurred", exception); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, exception.getMessage(), request); - } - - @ExceptionHandler(NakadiBaseException.class) - public ResponseEntity handleInternalError(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.error("Unexpected problem occurred", exception); - return Responses.create(Response.Status.INTERNAL_SERVER_ERROR, exception.getMessage(), request); - } - - @ExceptionHandler(TimelineException.class) - public ResponseEntity handleTimelineException(final TimelineException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - final Throwable cause = exception.getCause(); - if (cause instanceof InternalNakadiException) { - return Responses.create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); - } - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, exception.getMessage(), request); - } - - @ExceptionHandler(TopicCreationException.class) - public ResponseEntity handleTopicCreationException(final TopicCreationException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, exception.getMessage(), request); - } - - @ExceptionHandler(CursorConversionException.class) - public ResponseEntity handleCursorConversionException(final CursorConversionException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(ServiceTemporarilyUnavailableException.class) - public ResponseEntity handleServiceTemporarilyUnavailableException( - final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, exception.getMessage(), request); - } - - @ExceptionHandler(LimitReachedException.class) - public ResponseEntity handleLimitReachedException( - final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { - LOG.warn(exception.getMessage()); - return Responses.create(MoreStatus.TOO_MANY_REQUESTS, exception.getMessage(), request); - } - - @ExceptionHandler(DbWriteOperationsBlockedException.class) - public ResponseEntity handleDbWriteOperationsBlockedException( - final DbWriteOperationsBlockedException exception, final NativeWebRequest request) { - LOG.warn(exception.getMessage()); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, - "Database is currently in read-only mode", request); - } - - @ExceptionHandler(FeatureNotAvailableException.class) - public ResponseEntity handleFeatureNotAvailable( - final FeatureNotAvailableException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage()); - return Responses.create(Problem.valueOf(NOT_IMPLEMENTED, ex.getMessage()), request); - } - - @ExceptionHandler(CompactionException.class) - public ResponseEntity handleCompactionException(final CompactionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(EnrichmentException.class) - public ResponseEntity handleEnrichmentException(final EnrichmentException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(NoSuchPartitionStrategyException.class) - public ResponseEntity handleNoSuchPartitionStrategyException( - final NoSuchPartitionStrategyException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(PartitioningException.class) - public ResponseEntity handlePartitioningException(final PartitioningException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(InvalidPartitionKeyFieldsException.class) - public ResponseEntity handleInvalidPartitionKeyFieldsException( - final InvalidPartitionKeyFieldsException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(NoSuchEventTypeException.class) - public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(NOT_FOUND, exception.getMessage(), request); - } - - @ExceptionHandler(NoSuchSchemaException.class) - public ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(NOT_FOUND, exception.getMessage(), request); - } - - @ExceptionHandler(NoSuchSubscriptionException.class) - public ResponseEntity handleNoSuchSubscriptionException(final NoSuchSubscriptionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(NOT_FOUND, exception.getMessage(), request); - } - - @ExceptionHandler(NoStreamingSlotsAvailable.class) - public ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(CONFLICT, exception.getMessage(), request); - } - - @ExceptionHandler(UnprocessableSubscriptionException.class) - public ResponseEntity handleUnprocessableSubscriptionException( - final UnprocessableSubscriptionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(InvalidLimitException.class) - public ResponseEntity handleInvalidLimitException( - final InvalidLimitException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(BAD_REQUEST, exception.getMessage(), request); - } - - @ExceptionHandler(InvalidVersionNumberException.class) - public ResponseEntity handleInvalidVersionNumberException( - final InvalidVersionNumberException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(BAD_REQUEST, exception.getMessage(), request); - } - - @ExceptionHandler(DuplicatedStorageException.class) - public ResponseEntity handleDuplicatedStorageException( - final DuplicatedStorageException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(CONFLICT, exception.getMessage(), request); - } - - @ExceptionHandler(UnknownStorageTypeException.class) - public ResponseEntity handleUnknownStorageTypeException( - final UnknownStorageTypeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(UnprocessableEntityException.class) - public ResponseEntity handleUnprocessableEntityException( - final UnprocessableEntityException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @ExceptionHandler(NoSuchStorageException.class) - public ResponseEntity handleNoSuchStorageException( - final NoSuchStorageException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(NOT_FOUND, exception.getMessage(), request); - } - - @ExceptionHandler(StorageIsUsedException.class) - public ResponseEntity handleStorageIsUsedException( - final StorageIsUsedException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(FORBIDDEN, exception.getMessage(), request); - } -} diff --git a/src/main/java/org/zalando/nakadi/controller/HealthCheckController.java b/src/main/java/org/zalando/nakadi/controller/HealthCheckController.java index 0daaa19bb0..d181a904c9 100644 --- a/src/main/java/org/zalando/nakadi/controller/HealthCheckController.java +++ b/src/main/java/org/zalando/nakadi/controller/HealthCheckController.java @@ -4,13 +4,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.ws.rs.core.MediaType; - +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; import static org.springframework.http.ResponseEntity.ok; import static org.springframework.web.bind.annotation.RequestMethod.GET; @RestController -@RequestMapping(value = "/health", produces = MediaType.TEXT_PLAIN) +@RequestMapping(value = "/health", produces = TEXT_PLAIN_VALUE) public class HealthCheckController { @RequestMapping(method = GET) diff --git a/src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java b/src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java new file mode 100644 index 0000000000..0d83c8288b --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java @@ -0,0 +1,7 @@ +package org.zalando.nakadi.controller; + +import org.springframework.web.bind.annotation.ControllerAdvice; + +@ControllerAdvice +public class NakadiProblemControllerAdvice implements NakadiProblemHandling { +} diff --git a/src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java new file mode 100644 index 0000000000..db9c8c012d --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java @@ -0,0 +1,473 @@ +package org.zalando.nakadi.controller; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.google.common.base.CaseFormat; +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.exceptions.runtime.AccessDeniedException; +import org.zalando.nakadi.exceptions.runtime.CompactionException; +import org.zalando.nakadi.exceptions.runtime.ConflictException; +import org.zalando.nakadi.exceptions.runtime.CursorConversionException; +import org.zalando.nakadi.exceptions.runtime.CursorsAreEmptyException; +import org.zalando.nakadi.exceptions.runtime.DbWriteOperationsBlockedException; +import org.zalando.nakadi.exceptions.runtime.DuplicatedEventTypeNameException; +import org.zalando.nakadi.exceptions.runtime.DuplicatedStorageException; +import org.zalando.nakadi.exceptions.runtime.EnrichmentException; +import org.zalando.nakadi.exceptions.runtime.ErrorGettingCursorTimeLagException; +import org.zalando.nakadi.exceptions.runtime.EventTypeDeletionException; +import org.zalando.nakadi.exceptions.runtime.EventTypeOptionsValidationException; +import org.zalando.nakadi.exceptions.runtime.EventTypeUnavailableException; +import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; +import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; +import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; +import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; +import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; +import org.zalando.nakadi.exceptions.runtime.InvalidEventTypeException; +import org.zalando.nakadi.exceptions.runtime.InvalidLimitException; +import org.zalando.nakadi.exceptions.runtime.InvalidPartitionKeyFieldsException; +import org.zalando.nakadi.exceptions.runtime.InvalidStreamIdException; +import org.zalando.nakadi.exceptions.runtime.InvalidVersionNumberException; +import org.zalando.nakadi.exceptions.runtime.LimitReachedException; +import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; +import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; +import org.zalando.nakadi.exceptions.runtime.NoStreamingSlotsAvailable; +import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; +import org.zalando.nakadi.exceptions.runtime.NoSuchPartitionStrategyException; +import org.zalando.nakadi.exceptions.runtime.NoSuchSchemaException; +import org.zalando.nakadi.exceptions.runtime.NoSuchStorageException; +import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; +import org.zalando.nakadi.exceptions.runtime.NotFoundException; +import org.zalando.nakadi.exceptions.runtime.PartitioningException; +import org.zalando.nakadi.exceptions.runtime.RepositoryProblemException; +import org.zalando.nakadi.exceptions.runtime.RequestInProgressException; +import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; +import org.zalando.nakadi.exceptions.runtime.StorageIsUsedException; +import org.zalando.nakadi.exceptions.runtime.TimeLagStatsTimeoutException; +import org.zalando.nakadi.exceptions.runtime.TimelineException; +import org.zalando.nakadi.exceptions.runtime.TooManyPartitionsException; +import org.zalando.nakadi.exceptions.runtime.TopicCreationException; +import org.zalando.nakadi.exceptions.runtime.UnableProcessException; +import org.zalando.nakadi.exceptions.runtime.UnknownOperationException; +import org.zalando.nakadi.exceptions.runtime.UnknownStorageTypeException; +import org.zalando.nakadi.exceptions.runtime.UnprocessableEntityException; +import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; +import org.zalando.nakadi.exceptions.runtime.WrongInitialCursorsException; +import org.zalando.nakadi.exceptions.runtime.WrongStreamParametersException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.ProblemHandling; + +import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.NOT_IMPLEMENTED; +import static org.zalando.problem.Status.REQUEST_TIMEOUT; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.TOO_MANY_REQUESTS; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + + +public interface NakadiProblemHandling extends ProblemHandling { + + Logger LOG = LoggerFactory.getLogger(NakadiProblemHandling.class); + + String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; + + @Override + default String formatFieldName(final String fieldName) { + return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName); + } + + @Override + @ExceptionHandler + default ResponseEntity handleThrowable(final Throwable throwable, final NativeWebRequest request) { + final String errorTraceId = generateErrorTraceId(); + LOG.error("InternalServerError (" + errorTraceId + "):", throwable); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, "An internal error happened. Please report it. (" + + errorTraceId + ")"), request); + } + + default String generateErrorTraceId() { + return "ETI" + RandomStringUtils.randomAlphanumeric(24); + } + + @Override + @ExceptionHandler + default ResponseEntity handleMessageNotReadableException(final HttpMessageNotReadableException exception, + final NativeWebRequest request) { + /* + Unwrap nested JsonMappingException because the enclosing HttpMessageNotReadableException adds some ugly, Java + class and stacktrace like information. + */ + final Throwable mostSpecificCause = exception.getMostSpecificCause(); + final String message; + if (mostSpecificCause instanceof JsonMappingException) { + message = mostSpecificCause.getMessage(); + } else { + message = exception.getMessage(); + } + return create(Problem.valueOf(BAD_REQUEST, message), request); + } + + @ExceptionHandler(AccessDeniedException.class) + default ResponseEntity handleAccessDeniedException(final AccessDeniedException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(FORBIDDEN, exception.explain()), request); + } + + @ExceptionHandler(CompactionException.class) + default ResponseEntity handleCompactionException(final CompactionException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(ConflictException.class) + default ResponseEntity handleConflictException(final ConflictException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(CursorsAreEmptyException.class) + default ResponseEntity handleCursorsAreEmptyException(final RuntimeException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(CursorConversionException.class) + default ResponseEntity handleCursorConversionException(final CursorConversionException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(DbWriteOperationsBlockedException.class) + default ResponseEntity handleDbWriteOperationsBlockedException( + final DbWriteOperationsBlockedException exception, final NativeWebRequest request) { + LOG.warn(exception.getMessage()); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, + "Database is currently in read-only mode"), request); + } + + @ExceptionHandler(DuplicatedEventTypeNameException.class) + default ResponseEntity handleDuplicatedEventTypeNameException( + final DuplicatedEventTypeNameException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(DuplicatedStorageException.class) + default ResponseEntity handleDuplicatedStorageException( + final DuplicatedStorageException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(EnrichmentException.class) + default ResponseEntity handleEnrichmentException(final EnrichmentException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(ErrorGettingCursorTimeLagException.class) + default ResponseEntity handleErrorGettingCursorTimeLagException( + final ErrorGettingCursorTimeLagException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(EventTypeDeletionException.class) + default ResponseEntity handleEventTypeDeletionException(final EventTypeDeletionException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + + @ExceptionHandler(EventTypeOptionsValidationException.class) + default ResponseEntity handleEventTypeOptionsValidationException( + final EventTypeOptionsValidationException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(EventTypeUnavailableException.class) + default ResponseEntity handleEventTypeUnavailableException(final EventTypeUnavailableException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(FeatureNotAvailableException.class) + default ResponseEntity handleFeatureNotAvailableException( + final FeatureNotAvailableException ex, + final NativeWebRequest request) { + LOG.debug(ex.getMessage()); + return create(Problem.valueOf(NOT_IMPLEMENTED, ex.getMessage()), request); + } + + @ExceptionHandler(IllegalClientIdException.class) + default ResponseEntity handleIllegalClientIdException(final IllegalClientIdException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); + } + + @ExceptionHandler(InconsistentStateException.class) + default ResponseEntity handleInconsistentStateExcetpion(final InconsistentStateException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidCursorOperation.class) + default ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, + final NativeWebRequest request) { + LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); + } + + @ExceptionHandler(InvalidEventTypeException.class) + default ResponseEntity handleInvalidEventTypeException(final InvalidEventTypeException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidLimitException.class) + default ResponseEntity handleInvalidLimitException( + final InvalidLimitException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidPartitionKeyFieldsException.class) + default ResponseEntity handleInvalidPartitionKeyFieldsException( + final InvalidPartitionKeyFieldsException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidStreamIdException.class) + default ResponseEntity handleInvalidStreamIdException(final InvalidStreamIdException exception, + final NativeWebRequest request) { + LOG.warn("Stream id {} is not found: {}", exception.getStreamId(), exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidVersionNumberException.class) + default ResponseEntity handleInvalidVersionNumberException( + final InvalidVersionNumberException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); + } + + @ExceptionHandler(LimitReachedException.class) + default ResponseEntity handleLimitReachedException( + final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { + LOG.warn(exception.getMessage()); + return create(Problem.valueOf(TOO_MANY_REQUESTS, exception.getMessage()), request); + } + + @ExceptionHandler(NakadiBaseException.class) + default ResponseEntity handleNakadiBaseException(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.error("Unexpected problem occurred", exception); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + + @ExceptionHandler + default ResponseEntity handleNakadiRuntimeException(final NakadiRuntimeException exception, + final NativeWebRequest request) throws Exception { + final Throwable cause = exception.getCause(); + if (cause instanceof InternalNakadiException) { + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + throw exception.getException(); + } + + @ExceptionHandler(NotFoundException.class) + default ResponseEntity handleNotFoundException(final NotFoundException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(NoStreamingSlotsAvailable.class) + default ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(NoSuchEventTypeException.class) + default ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(NoSuchPartitionStrategyException.class) + default ResponseEntity handleNoSuchPartitionStrategyException( + final NoSuchPartitionStrategyException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(NoSuchSchemaException.class) + default ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(NoSuchStorageException.class) + default ResponseEntity handleNoSuchStorageException( + final NoSuchStorageException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(NoSuchSubscriptionException.class) + default ResponseEntity handleNoSuchSubscriptionException(final NoSuchSubscriptionException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(PartitioningException.class) + default ResponseEntity handlePartitioningException(final PartitioningException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(RepositoryProblemException.class) + default ResponseEntity handleRepositoryProblemException(final RepositoryProblemException exception, + final NativeWebRequest request) { + LOG.error("Repository problem occurred", exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(RequestInProgressException.class) + default ResponseEntity handleRequestInProgressException(final RequestInProgressException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(ServiceTemporarilyUnavailableException.class) + default ResponseEntity handleServiceTemporarilyUnavailableException( + final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(StorageIsUsedException.class) + default ResponseEntity handleStorageIsUsedException( + final StorageIsUsedException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); + } + + @ExceptionHandler(TimeLagStatsTimeoutException.class) + default ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException exception, + final NativeWebRequest request) { + LOG.warn(exception.getMessage()); + return create(Problem.valueOf(REQUEST_TIMEOUT, exception.getMessage()), request); + } + + @ExceptionHandler(TimelineException.class) + default ResponseEntity handleTimelineException(final TimelineException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + final Throwable cause = exception.getCause(); + if (cause instanceof InternalNakadiException) { + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(TooManyPartitionsException.class) + default ResponseEntity handleTooManyPartitionsException(final TooManyPartitionsException exception, + final NativeWebRequest request) { + LOG.debug("Error occurred when working with subscriptions", exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(TopicCreationException.class) + default ResponseEntity handleTopicCreationException(final TopicCreationException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(UnableProcessException.class) + default ResponseEntity handleUnableProcessException(final UnableProcessException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(UnknownOperationException.class) + default ResponseEntity handleUnknownOperationException(final RuntimeException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, "There was a problem processing your request."), request); + } + + @ExceptionHandler(UnknownStorageTypeException.class) + default ResponseEntity handleUnknownStorageTypeException( + final UnknownStorageTypeException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(UnprocessableEntityException.class) + default ResponseEntity handleUnprocessableEntityException( + final UnprocessableEntityException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(UnprocessableSubscriptionException.class) + default ResponseEntity handleUnprocessableSubscriptionException( + final UnprocessableSubscriptionException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(WrongInitialCursorsException.class) + default ResponseEntity handleWrongInitialCursorsException(final WrongInitialCursorsException exception, + final NativeWebRequest request) { + LOG.debug("Error occurred when working with subscriptions", exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(WrongStreamParametersException.class) + default ResponseEntity handleWrongStreamParametersException(final WrongStreamParametersException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/PartitionsController.java b/src/main/java/org/zalando/nakadi/controller/PartitionsController.java index 19e37ac713..53aebac950 100644 --- a/src/main/java/org/zalando/nakadi/controller/PartitionsController.java +++ b/src/main/java/org/zalando/nakadi/controller/PartitionsController.java @@ -5,7 +5,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -20,7 +19,6 @@ import org.zalando.nakadi.domain.Timeline; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidCursorException; -import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NotFoundException; @@ -33,31 +31,27 @@ import org.zalando.nakadi.view.Cursor; import org.zalando.nakadi.view.CursorLag; import org.zalando.nakadi.view.EventTypePartitionView; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.annotation.Nullable; -import javax.ws.rs.core.Response; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.springframework.http.ResponseEntity.ok; -import static org.zalando.problem.spring.web.advice.Responses.create; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class PartitionsController { +public class PartitionsController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(PartitionsController.class); private final TimelineService timelineService; private final CursorConverter cursorConverter; private final CursorOperationsService cursorOperationsService; - private static final String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; private final EventTypeRepository eventTypeRepository; private final AuthorizationValidator authorizationValidator; @@ -136,24 +130,11 @@ public ResponseEntity getPartition( LOG.error("Could not get partition. Respond with SERVICE_UNAVAILABLE.", e); return create(Problem.valueOf(SERVICE_UNAVAILABLE, e.getMessage()), request); } catch (final InvalidCursorException e) { - return create(Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); } } - @ExceptionHandler(InvalidCursorOperation.class) - public ResponseEntity invalidCursorOperation(final InvalidCursorOperation e, - final NativeWebRequest request) { - LOG.debug("User provided invalid cursor for operation. Reason: " + e.getReason(), e); - return Responses.create(Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity notFound(final NotFoundException ex, final NativeWebRequest request) { - LOG.error(ex.getMessage(), ex); - return Responses.create(Response.Status.NOT_FOUND, ex.getMessage(), request); - } - private CursorLag getCursorLag(final String eventTypeName, final String partition, final String consumedOffset) throws InternalNakadiException, NoSuchEventTypeException, InvalidCursorException, ServiceTemporarilyUnavailableException { diff --git a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java index b8d0faa9b8..e91e202fe6 100644 --- a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java @@ -6,7 +6,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.Errors; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,32 +17,28 @@ import org.zalando.nakadi.domain.SubscriptionBase; import org.zalando.nakadi.exceptions.runtime.DuplicatedSubscriptionException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; -import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.SubscriptionUpdateConflictException; -import org.zalando.nakadi.exceptions.runtime.TooManyPartitionsException; import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; -import org.zalando.nakadi.exceptions.runtime.WrongInitialCursorsException; import org.zalando.nakadi.plugin.api.ApplicationService; import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.service.subscription.SubscriptionService; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.validation.Valid; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; +import static org.apache.http.HttpHeaders.CONTENT_LOCATION; import static org.springframework.http.HttpStatus.OK; import static org.zalando.nakadi.service.FeatureToggleService.Feature.DISABLE_SUBSCRIPTION_CREATION; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class PostSubscriptionController { +public class PostSubscriptionController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(PostSubscriptionController.class); @@ -65,15 +60,15 @@ public ResponseEntity createOrGetSubscription(@Valid @RequestBody final Subsc final Errors errors, final NativeWebRequest request) { if (errors.hasErrors()) { - return Responses.create(new ValidationProblem(errors), request); + return create(new ValidationProblem(errors), request); } try { return ok(subscriptionService.getExistingSubscription(subscriptionBase)); } catch (final NoSuchSubscriptionException e) { if (featureToggleService.isFeatureEnabled(DISABLE_SUBSCRIPTION_CREATION)) { - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, - "Subscription creation is temporarily unavailable", request); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, + "Subscription creation is temporarily unavailable"), request); } try { final Subscription subscription = subscriptionService.createSubscription(subscriptionBase); @@ -93,15 +88,15 @@ public ResponseEntity updateSubscription( final Errors errors, final NativeWebRequest request) { if (errors.hasErrors()) { - return Responses.create(new ValidationProblem(errors), request); + return create(new ValidationProblem(errors), request); } try { subscriptionService.updateSubscription(subscriptionId, subscription); return ResponseEntity.noContent().build(); } catch (final SubscriptionUpdateConflictException ex) { - return Responses.create(MoreStatus.UNPROCESSABLE_ENTITY, ex.getMessage(), request); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, ex.getMessage()), request); } catch (final NoSuchSubscriptionException ex) { - return Responses.create(Problem.valueOf(NOT_FOUND, ex.getMessage()), request); + return create(Problem.valueOf(NOT_FOUND, ex.getMessage()), request); } } @@ -114,16 +109,7 @@ private ResponseEntity prepareLocationResponse(final Subscription subscriptio final UriComponents location = subscriptionService.getSubscriptionUri(subscription); return ResponseEntity.status(HttpStatus.CREATED) .location(location.toUri()) - .header(HttpHeaders.CONTENT_LOCATION, location.toString()) + .header(CONTENT_LOCATION, location.toString()) .body(subscription); } - - @ExceptionHandler({ - WrongInitialCursorsException.class, - TooManyPartitionsException.class}) - public ResponseEntity handleUnprocessableSubscription(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug("Error occurred when working with subscriptions", exception); - return Responses.create(MoreStatus.UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index 6b0db0317b..2d4923005e 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -5,35 +5,27 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.ResourceAuthorization; -import org.zalando.nakadi.exceptions.runtime.UnableProcessException; -import org.zalando.nakadi.exceptions.runtime.UnknownOperationException; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; import org.zalando.nakadi.security.Client; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.BlacklistService; import org.zalando.nakadi.service.FeatureToggleService; -import org.zalando.problem.MoreStatus; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.validation.Valid; -import javax.ws.rs.core.Response; import static org.zalando.nakadi.domain.AdminResource.ADMIN_RESOURCE; @RestController @RequestMapping(value = "/settings") -public class SettingsController { +public class SettingsController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(SettingsController.class); private final BlacklistService blacklistService; @@ -116,21 +108,6 @@ public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthoriz return ResponseEntity.ok().build(); } - @ExceptionHandler(UnknownOperationException.class) - public ResponseEntity handleUnknownOperationException(final RuntimeException ex, - final NativeWebRequest request) { - LOG.error(ex.getMessage(), ex); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, - "There was a problem processing your request.", request); - } - - @ExceptionHandler(UnableProcessException.class) - public ResponseEntity handleUnableProcessException(final RuntimeException ex, - final NativeWebRequest request) { - LOG.error(ex.getMessage(), ex); - return Responses.create(MoreStatus.UNPROCESSABLE_ENTITY, ex.getMessage(), request); - } - private boolean isNotAdmin(final Client client) { return !client.getClientId().equals(securitySettings.getAdminClientId()); } diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java index 784b037694..ec50774f87 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java @@ -4,7 +4,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -13,34 +12,25 @@ import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.SubscriptionEventTypeStats; -import org.zalando.nakadi.exceptions.runtime.ErrorGettingCursorTimeLagException; -import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; -import org.zalando.nakadi.exceptions.runtime.TimeLagStatsTimeoutException; import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.service.subscription.SubscriptionService; import org.zalando.nakadi.service.subscription.SubscriptionService.StatsMode; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.annotation.Nullable; -import javax.ws.rs.core.Response; import java.util.Set; -import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.ResponseEntity.status; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; @RestController @RequestMapping(value = "/subscriptions") -public class SubscriptionController { +public class SubscriptionController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(SubscriptionController.class); @@ -90,48 +80,4 @@ public ItemsWrapper getSubscriptionStats( final StatsMode statsMode = showTimeLag ? StatsMode.TIMELAG : StatsMode.NORMAL; return subscriptionService.getSubscriptionStat(subscriptionId, statsMode); } - - @ExceptionHandler(FeatureNotAvailableException.class) - public ResponseEntity handleFeatureTurnedOff(final FeatureNotAvailableException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(Problem.valueOf(NOT_IMPLEMENTED, ex.getMessage()), request); - } - - @ExceptionHandler(ErrorGettingCursorTimeLagException.class) - public ResponseEntity handleTimeLagException(final ErrorGettingCursorTimeLagException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(Problem.valueOf(UNPROCESSABLE_ENTITY, ex.getMessage()), request); - } - - @ExceptionHandler(InconsistentStateException.class) - public ResponseEntity handleInconsistentState(final InconsistentStateException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create( - Problem.valueOf( - SERVICE_UNAVAILABLE, - ex.getMessage()), - request); - } - - @ExceptionHandler(ServiceTemporarilyUnavailableException.class) - public ResponseEntity handleServiceTemporarilyUnavailable(final ServiceTemporarilyUnavailableException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create( - Problem.valueOf( - SERVICE_UNAVAILABLE, - ex.getMessage()), - request); - } - - @ExceptionHandler(TimeLagStatsTimeoutException.class) - public ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException e, - final NativeWebRequest request) { - LOG.warn(e.getMessage()); - return Responses.create(Response.Status.REQUEST_TIMEOUT, e.getMessage(), request); - } - } diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java index 5d9b261677..20969a7940 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java @@ -9,15 +9,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import org.zalando.nakadi.config.NakadiSettings; import org.zalando.nakadi.domain.Subscription; @@ -40,7 +37,6 @@ import org.zalando.nakadi.util.FlowIdUtils; import org.zalando.nakadi.view.UserStreamParameters; import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; @@ -50,16 +46,16 @@ import java.io.OutputStream; import java.util.concurrent.atomic.AtomicBoolean; -import static javax.ws.rs.core.Response.Status.CONFLICT; -import static javax.ws.rs.core.Response.Status.FORBIDDEN; -import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.zalando.nakadi.metrics.MetricUtils.metricNameForSubscription; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class SubscriptionStreamController { +public class SubscriptionStreamController extends NakadiProblemControllerAdvice { public static final String CONSUMERS_COUNT_METRIC_NAME = "consumers"; private static final Logger LOG = LoggerFactory.getLogger(SubscriptionStreamController.class); @@ -245,12 +241,4 @@ private void writeProblemResponse(final HttpServletResponse response, response.setContentType("application/problem+json"); jsonMapper.writer().writeValue(outputStream, problem); } - - @ExceptionHandler(WrongStreamParametersException.class) - public ResponseEntity invalidEventTypeException(final WrongStreamParametersException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - } diff --git a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java index c58c3d329f..28a62e3af9 100644 --- a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java +++ b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java @@ -5,36 +5,27 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.exceptions.runtime.AccessDeniedException; -import org.zalando.nakadi.exceptions.runtime.ConflictException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; -import org.zalando.nakadi.exceptions.runtime.NotFoundException; import org.zalando.nakadi.exceptions.runtime.RepositoryProblemException; import org.zalando.nakadi.exceptions.runtime.TimelineException; -import org.zalando.nakadi.exceptions.runtime.TimelinesNotSupportedException; import org.zalando.nakadi.exceptions.runtime.TopicRepositoryException; -import org.zalando.nakadi.exceptions.runtime.UnableProcessException; import org.zalando.nakadi.service.timeline.TimelineService; import org.zalando.nakadi.view.TimelineRequest; import org.zalando.nakadi.view.TimelineView; -import org.zalando.problem.MoreStatus; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; import java.util.stream.Collectors; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + @RestController -@RequestMapping(value = "/event-types/{name}/timelines", produces = MediaType.APPLICATION_JSON) -public class TimelinesController { +@RequestMapping(value = "/event-types/{name}/timelines", produces = APPLICATION_JSON_VALUE) +public class TimelinesController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(TimelinesController.class); @@ -60,25 +51,4 @@ public ResponseEntity getTimelines(@PathVariable("name") final String eventTy .map(TimelineView::new) .collect(Collectors.toList())); } - - @ExceptionHandler({ - UnableProcessException.class, - TimelinesNotSupportedException.class}) - public ResponseEntity unprocessable(final UnableProcessException ex, final NativeWebRequest request) { - LOG.error(ex.getMessage(), ex); - return Responses.create(MoreStatus.UNPROCESSABLE_ENTITY, ex.getMessage(), request); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity notFound(final NotFoundException ex, final NativeWebRequest request) { - LOG.error(ex.getMessage(), ex); - return Responses.create(Response.Status.NOT_FOUND, ex.getMessage(), request); - } - - @ExceptionHandler(ConflictException.class) - public ResponseEntity conflict(final ConflictException ex, final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(Response.Status.CONFLICT, ex.getMessage(), request); - } - } diff --git a/src/main/java/org/zalando/nakadi/controller/VersionController.java b/src/main/java/org/zalando/nakadi/controller/VersionController.java index 6a6417fcd0..8c7473cb1a 100644 --- a/src/main/java/org/zalando/nakadi/controller/VersionController.java +++ b/src/main/java/org/zalando/nakadi/controller/VersionController.java @@ -8,15 +8,15 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.ws.rs.core.MediaType; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.GET; @RestController -@RequestMapping(value = "/version", produces = MediaType.APPLICATION_JSON) +@RequestMapping(value = "/version", produces = APPLICATION_JSON_VALUE) @Profile("!test") public class VersionController { diff --git a/src/main/java/org/zalando/nakadi/problem/ValidationProblem.java b/src/main/java/org/zalando/nakadi/problem/ValidationProblem.java index b504e9e9e0..803c5f136f 100644 --- a/src/main/java/org/zalando/nakadi/problem/ValidationProblem.java +++ b/src/main/java/org/zalando/nakadi/problem/ValidationProblem.java @@ -4,14 +4,14 @@ import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; -import org.zalando.problem.MoreStatus; -import org.zalando.problem.Problem; +import org.zalando.problem.StatusType; +import org.zalando.problem.ThrowableProblem; -import javax.ws.rs.core.Response; import java.net.URI; -import java.util.Optional; -public class ValidationProblem implements Problem { +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +public class ValidationProblem extends ThrowableProblem { private final Errors errors; private static final String TYPE_VALUE = "http://httpstatus.es/422"; @@ -33,13 +33,13 @@ public String getTitle() { } @Override - public Response.StatusType getStatus() { - return MoreStatus.UNPROCESSABLE_ENTITY; + public StatusType getStatus() { + return UNPROCESSABLE_ENTITY; } @Override - public Optional getDetail() { - return Optional.of(buildErrorMessage()); + public String getDetail() { + return buildErrorMessage(); } private String buildErrorMessage() { diff --git a/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java b/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java index 769d6d44cc..28b83a8783 100644 --- a/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java +++ b/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java @@ -24,9 +24,9 @@ import java.util.Optional; import java.util.zip.GZIPInputStream; -import static javax.ws.rs.HttpMethod.POST; -import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING; -import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; +import static org.springframework.http.HttpHeaders.CONTENT_ENCODING; +import static org.springframework.http.HttpMethod.POST; +import static org.zalando.problem.Status.NOT_ACCEPTABLE; public class GzipBodyRequestFilter implements Filter { diff --git a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java index 810f287dd3..6db7fc6a2d 100644 --- a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java @@ -33,8 +33,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; @@ -49,7 +47,9 @@ import static org.zalando.nakadi.utils.TestUtils.buildDefaultEventType; import static org.zalando.nakadi.utils.TestUtils.buildTimelineWithTopic; import static org.zalando.nakadi.utils.TestUtils.invalidProblem; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class CursorsControllerTest { diff --git a/src/test/java/org/zalando/nakadi/controller/EventStreamControllerTest.java b/src/test/java/org/zalando/nakadi/controller/EventStreamControllerTest.java index 7166c4f9ef..cae5f57815 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventStreamControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/EventStreamControllerTest.java @@ -63,12 +63,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; -import static javax.ws.rs.core.Response.Status.FORBIDDEN; -import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -87,7 +81,13 @@ import static org.zalando.nakadi.metrics.MetricUtils.metricNameFor; import static org.zalando.nakadi.utils.TestUtils.buildTimelineWithTopic; import static org.zalando.nakadi.utils.TestUtils.mockAccessDeniedException; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; +import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.PRECONDITION_FAILED; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class EventStreamControllerTest { diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeAuthorizationTest.java b/src/test/java/org/zalando/nakadi/controller/EventTypeAuthorizationTest.java index 00a10cec0b..60f932e2ea 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeAuthorizationTest.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeAuthorizationTest.java @@ -8,10 +8,8 @@ import org.zalando.nakadi.plugin.api.authz.AuthorizationService; import org.zalando.nakadi.plugin.api.authz.Resource; import org.zalando.nakadi.utils.EventTypeTestBuilder; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; -import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Optional; @@ -21,6 +19,8 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class EventTypeAuthorizationTest extends EventTypeControllerTestCase { @@ -48,7 +48,7 @@ public void whenPUTNotAuthorizedThen403() throws Exception { putEventType(eventType, eventType.getName()) .andExpect(status().isForbidden()) - .andExpect(content().string(matchesProblem(Problem.valueOf(Response.Status.FORBIDDEN, + .andExpect(content().string(matchesProblem(Problem.valueOf(FORBIDDEN, "Access on ADMIN event-type:" + eventType.getName() + " denied")))); } @@ -76,7 +76,7 @@ public void whenPUTUnlimitedRetentionTimeByUserThen422() throws Exception { putEventType(eventType, eventType.getName()) .andExpect(status().isUnprocessableEntity()) - .andExpect(content().string(matchesProblem(Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, + .andExpect(content().string(matchesProblem(Problem.valueOf(UNPROCESSABLE_ENTITY, "Field \"options.retention_time\" can not be more than 345600000")))); } @@ -90,7 +90,7 @@ public void whenPUTNullAuthorizationForExistingAuthorization() throws Exception putEventType(newEventType, newEventType.getName()) .andExpect(status().isUnprocessableEntity()) - .andExpect(content().string(matchesProblem(Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, + .andExpect(content().string(matchesProblem(Problem.valueOf(UNPROCESSABLE_ENTITY, "Changing authorization object to `null` is not possible due to existing one")))); } @@ -105,7 +105,7 @@ public void whenDELETENotAuthorized200() throws Exception { deleteEventType(eventType.getName()) .andExpect(status().isForbidden()) - .andExpect(content().string(matchesProblem(Problem.valueOf(Response.Status.FORBIDDEN, + .andExpect(content().string(matchesProblem(Problem.valueOf(FORBIDDEN, "Access on ADMIN event-type:" + eventType.getName() + " denied")))); } diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTest.java b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTest.java index 8b669eb103..56c10cb678 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTest.java @@ -25,10 +25,10 @@ import org.zalando.nakadi.domain.ResourceAuthorizationAttribute; import org.zalando.nakadi.domain.Subscription; import org.zalando.nakadi.domain.Timeline; -import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; -import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.DuplicatedEventTypeNameException; +import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidEventTypeException; +import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.TopicConfigException; import org.zalando.nakadi.exceptions.runtime.TopicCreationException; import org.zalando.nakadi.exceptions.runtime.UnableProcessException; @@ -36,11 +36,9 @@ import org.zalando.nakadi.repository.TopicRepository; import org.zalando.nakadi.utils.EventTypeTestBuilder; import org.zalando.nakadi.utils.TestUtils; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; import org.zalando.problem.ThrowableProblem; -import javax.ws.rs.core.Response; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -49,8 +47,6 @@ import java.util.Optional; import java.util.concurrent.TimeoutException; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; @@ -76,6 +72,11 @@ import static org.zalando.nakadi.utils.TestUtils.createInvalidEventTypeExceptionProblem; import static org.zalando.nakadi.utils.TestUtils.invalidProblem; import static org.zalando.nakadi.utils.TestUtils.randomValidEventTypeName; +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class EventTypeControllerTest extends EventTypeControllerTestCase { @@ -424,7 +425,7 @@ public void whenPostAndAuthorizationInvalidThen422() throws Exception { doThrow(new UnableProcessException("dummy")).when(authorizationValidator).validateAuthorization(any()); - postETAndExpect422WithProblem(eventType, Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, "dummy")); + postETAndExpect422WithProblem(eventType, Problem.valueOf(UNPROCESSABLE_ENTITY, "dummy")); } @Test @@ -499,7 +500,7 @@ public void whenPOSTInvalidSchemaThen422() throws Exception { @Test public void whenPostDuplicatedEventTypeReturn409() throws Exception { - final Problem expectedProblem = Problem.valueOf(Response.Status.CONFLICT, "some-name"); + final Problem expectedProblem = Problem.valueOf(CONFLICT, "some-name"); doThrow(new DuplicatedEventTypeNameException("some-name")).when(eventTypeRepository).saveEventType(any( EventTypeBase.class)); @@ -569,7 +570,7 @@ public void whenDeleteEventTypeThatHasSubscriptionsThenConflict() throws Excepti .listSubscriptions(eq(ImmutableSet.of(eventType.getName())), eq(Optional.empty()), anyInt(), anyInt())) .thenReturn(ImmutableList.of(mock(Subscription.class))); - final Problem expectedProblem = Problem.valueOf(Response.Status.CONFLICT, + final Problem expectedProblem = Problem.valueOf(CONFLICT, "Can't remove event type " + eventType.getName() + ", as it has subscriptions"); deleteEventType(eventType.getName()) @@ -582,7 +583,7 @@ public void whenDeleteEventTypeThatHasSubscriptionsThenConflict() throws Excepti public void whenDeleteEventTypeAndNakadiExceptionThen500() throws Exception { final String eventTypeName = randomValidEventTypeName(); - final Problem expectedProblem = Problem.valueOf(Response.Status.INTERNAL_SERVER_ERROR, + final Problem expectedProblem = Problem.valueOf(INTERNAL_SERVER_ERROR, "Failed to delete event type " + eventTypeName); doThrow(new InternalNakadiException("dummy message")) @@ -597,7 +598,7 @@ public void whenDeleteEventTypeAndNakadiExceptionThen500() throws Exception { @Test public void whenPersistencyErrorThen500() throws Exception { - final Problem expectedProblem = Problem.valueOf(Response.Status.INTERNAL_SERVER_ERROR); + final Problem expectedProblem = Problem.valueOf(INTERNAL_SERVER_ERROR); doThrow(InternalNakadiException.class).when(eventTypeRepository).saveEventType(any(EventType.class)); diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java index 11b80985ba..8187ee959d 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java @@ -118,7 +118,7 @@ public void init() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new ExceptionHandling()) + .setControllerAdvice(new NakadiProblemControllerAdvice()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/ExceptionHandlingTest.java b/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java similarity index 79% rename from src/test/java/org/zalando/nakadi/controller/ExceptionHandlingTest.java rename to src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java index bcc9c9169f..acb108364a 100644 --- a/src/test/java/org/zalando/nakadi/controller/ExceptionHandlingTest.java +++ b/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java @@ -10,21 +10,21 @@ import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; import org.zalando.problem.Problem; -public class ExceptionHandlingTest { +public class NakadiProblemHandlingTest { private static final String OAUTH2_SCOPE_WRITE = "oauth2.scope.write"; @Test public void testIllegalClientIdException() { - final ExceptionHandling exceptionHandling = new ExceptionHandling(); + final NakadiProblemHandling nakadiProblemHandling = new NakadiProblemControllerAdvice(); final NativeWebRequest mockedRequest = Mockito.mock(NativeWebRequest.class); Mockito.when(mockedRequest.getHeader(Matchers.any())).thenReturn(""); - final ResponseEntity problemResponseEntity = exceptionHandling.handleIllegalClientIdException( + final ResponseEntity problemResponseEntity = nakadiProblemHandling.handleIllegalClientIdException( new IllegalClientIdException("You don't have access to this event type"), mockedRequest); Assert.assertEquals(problemResponseEntity.getStatusCode(), HttpStatus.FORBIDDEN); - Assert.assertEquals(problemResponseEntity.getBody().getDetail().get(), + Assert.assertEquals(problemResponseEntity.getBody().getDetail(), "You don't have access to this event type"); } } \ No newline at end of file diff --git a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java index f7edc3ca70..08e39befde 100644 --- a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java @@ -38,8 +38,6 @@ import java.util.List; import java.util.Optional; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doNothing; @@ -51,6 +49,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; import static org.zalando.nakadi.utils.TestUtils.buildTimeline; import static org.zalando.nakadi.utils.TestUtils.mockAccessDeniedException; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; public class PartitionsControllerTest { @@ -111,7 +111,7 @@ public void before() throws InternalNakadiException, NoSuchEventTypeException { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new ExceptionHandling()) + .setControllerAdvice(new NakadiProblemControllerAdvice()) .build(); } @@ -144,7 +144,7 @@ public void whenListPartitionsAndNakadiExceptionThenServiceUnavaiable() throws E when(timelineService.getActiveTimelinesOrdered(eq(TEST_EVENT_TYPE))) .thenThrow(ServiceTemporarilyUnavailableException.class); - final ThrowableProblem expectedProblem = Problem.valueOf(SERVICE_UNAVAILABLE, null); + final ThrowableProblem expectedProblem = Problem.valueOf(SERVICE_UNAVAILABLE, ""); mockMvc.perform( get(String.format("/event-types/%s/partitions", TEST_EVENT_TYPE))) .andExpect(status().isServiceUnavailable()) @@ -262,7 +262,7 @@ public void whenGetPartitionAndNakadiExceptionThenServiceUnavaiable() throws Exc when(timelineService.getActiveTimelinesOrdered(eq(TEST_EVENT_TYPE))) .thenThrow(ServiceTemporarilyUnavailableException.class); - final ThrowableProblem expectedProblem = Problem.valueOf(SERVICE_UNAVAILABLE, null); + final ThrowableProblem expectedProblem = Problem.valueOf(SERVICE_UNAVAILABLE, ""); mockMvc.perform( get(String.format("/event-types/%s/partitions/%s", TEST_EVENT_TYPE, TEST_PARTITION))) .andExpect(status().isServiceUnavailable()) diff --git a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java index 2e94ce20f5..b315432b94 100644 --- a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java @@ -38,7 +38,7 @@ import static org.zalando.nakadi.service.FeatureToggleService.Feature.DISABLE_SUBSCRIPTION_CREATION; import static org.zalando.nakadi.utils.RandomSubscriptionBuilder.builder; import static org.zalando.nakadi.utils.TestUtils.invalidProblem; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; public class PostSubscriptionControllerTest { @@ -67,7 +67,7 @@ public PostSubscriptionControllerTest() throws Exception { mockMvcBuilder = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new ExceptionHandling()) + .setControllerAdvice(new NakadiProblemControllerAdvice()) .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()); } diff --git a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java index d31eefb27d..ac06ae8544 100644 --- a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java @@ -50,7 +50,6 @@ import org.zalando.problem.Problem; import org.zalando.problem.ThrowableProblem; -import javax.ws.rs.core.Response; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -61,9 +60,6 @@ import java.util.Set; import static java.text.MessageFormat.format; -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; -import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; @@ -83,6 +79,9 @@ import static org.zalando.nakadi.utils.RandomSubscriptionBuilder.builder; import static org.zalando.nakadi.utils.TestUtils.buildTimelineWithTopic; import static org.zalando.nakadi.utils.TestUtils.createRandomSubscriptions; +import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; public class SubscriptionControllerTest { @@ -131,7 +130,7 @@ public SubscriptionControllerTest() throws Exception { mockMvcBuilder = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new ExceptionHandling()) + .setControllerAdvice(new NakadiProblemControllerAdvice()) .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()); } @@ -150,7 +149,7 @@ public void whenGetNoneExistingSubscriptionThenNotFound() throws Exception { final Subscription subscription = builder().build(); when(subscriptionRepository.getSubscription(subscription.getId())) .thenThrow(new NoSuchSubscriptionException("dummy-message")); - final ThrowableProblem expectedProblem = Problem.valueOf(Response.Status.NOT_FOUND, "dummy-message"); + final ThrowableProblem expectedProblem = Problem.valueOf(NOT_FOUND, "dummy-message"); getSubscription(subscription.getId()) .andExpect(status().isNotFound()) diff --git a/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java b/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java index 7c3f9e20cc..22ab92a9ed 100644 --- a/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java @@ -42,7 +42,7 @@ public TimelinesControllerTest() { mockMvc = MockMvcBuilders.standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(securitySettings, featureToggleService)) - .setControllerAdvice(new ExceptionHandling()) + .setControllerAdvice(new NakadiProblemControllerAdvice()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/utils/TestUtils.java b/src/test/java/org/zalando/nakadi/utils/TestUtils.java index f4b7f2c32e..a72da43b16 100644 --- a/src/test/java/org/zalando/nakadi/utils/TestUtils.java +++ b/src/test/java/org/zalando/nakadi/utils/TestUtils.java @@ -27,7 +27,6 @@ import org.zalando.nakadi.plugin.api.authz.Resource; import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.service.NakadiKpiPublisher; -import org.zalando.problem.MoreStatus; import org.zalando.problem.Problem; import java.io.IOException; @@ -51,6 +50,7 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; import static org.zalando.nakadi.utils.RandomSubscriptionBuilder.builder; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; public class TestUtils { @@ -177,7 +177,7 @@ public static Problem invalidProblem(final String field, final String descriptio } public static Problem createInvalidEventTypeExceptionProblem(final String message) { - return Problem.valueOf(MoreStatus.UNPROCESSABLE_ENTITY, message); + return Problem.valueOf(UNPROCESSABLE_ENTITY, message); } public static void waitFor(final Runnable runnable) { From 6a76edaabbf7fb9e1127588d564fa81f8e5c7fa0 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 30 Aug 2018 16:28:48 +0200 Subject: [PATCH 02/23] Clean up controllers --- .../zalando/nakadi/controller/CursorsController.java | 4 ---- .../zalando/nakadi/controller/EventTypeController.java | 8 -------- .../nakadi/controller/PostSubscriptionController.java | 8 -------- .../zalando/nakadi/controller/SettingsController.java | 3 --- .../zalando/nakadi/controller/StoragesController.java | 6 +----- .../nakadi/controller/SubscriptionController.java | 10 +--------- .../controller/SubscriptionStreamController.java | 4 ---- .../zalando/nakadi/controller/TimelinesController.java | 4 ---- .../nakadi/controller/CursorsControllerTest.java | 3 +-- .../nakadi/controller/EventTypeControllerTestCase.java | 2 +- .../controller/PostSubscriptionControllerTest.java | 2 +- .../nakadi/controller/StoragesControllerTest.java | 2 +- .../nakadi/controller/SubscriptionControllerTest.java | 2 +- 13 files changed, 7 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/controller/CursorsController.java b/src/main/java/org/zalando/nakadi/controller/CursorsController.java index b375482dd4..a40e61d796 100644 --- a/src/main/java/org/zalando/nakadi/controller/CursorsController.java +++ b/src/main/java/org/zalando/nakadi/controller/CursorsController.java @@ -27,7 +27,6 @@ import org.zalando.nakadi.service.CursorConverter; import org.zalando.nakadi.service.CursorTokenService; import org.zalando.nakadi.service.CursorsService; -import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.view.CursorCommitResult; import org.zalando.nakadi.view.SubscriptionCursor; import org.zalando.nakadi.view.SubscriptionCursorWithoutToken; @@ -53,17 +52,14 @@ public class CursorsController extends NakadiProblemControllerAdvice { private static final Logger LOG = LoggerFactory.getLogger(CursorsController.class); private final CursorsService cursorsService; - private final FeatureToggleService featureToggleService; private final CursorConverter cursorConverter; private final CursorTokenService cursorTokenService; @Autowired public CursorsController(final CursorsService cursorsService, - final FeatureToggleService featureToggleService, final CursorConverter cursorConverter, final CursorTokenService cursorTokenService) { this.cursorsService = cursorsService; - this.featureToggleService = featureToggleService; this.cursorConverter = cursorConverter; this.cursorTokenService = cursorTokenService; } diff --git a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java index 304541254c..bc1462a14e 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java @@ -1,8 +1,6 @@ package org.zalando.nakadi.controller; import com.google.common.collect.Lists; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -32,7 +30,6 @@ import org.zalando.nakadi.exceptions.runtime.TopicConfigException; import org.zalando.nakadi.exceptions.runtime.TopicCreationException; import org.zalando.nakadi.exceptions.runtime.UnableProcessException; -import org.zalando.nakadi.plugin.api.ApplicationService; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.service.AdminService; @@ -51,23 +48,18 @@ @RequestMapping(value = "/event-types") public class EventTypeController extends NakadiProblemControllerAdvice { - private static final Logger LOG = LoggerFactory.getLogger(TimelinesController.class); - private final EventTypeService eventTypeService; private final FeatureToggleService featureToggleService; - private final ApplicationService applicationService; private final AdminService adminService; private final NakadiSettings nakadiSettings; @Autowired public EventTypeController(final EventTypeService eventTypeService, final FeatureToggleService featureToggleService, - final ApplicationService applicationService, final AdminService adminService, final NakadiSettings nakadiSettings) { this.eventTypeService = eventTypeService; this.featureToggleService = featureToggleService; - this.applicationService = applicationService; this.adminService = adminService; this.nakadiSettings = nakadiSettings; } diff --git a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java index e91e202fe6..51df4938ab 100644 --- a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java @@ -1,7 +1,5 @@ package org.zalando.nakadi.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,7 +19,6 @@ import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.SubscriptionUpdateConflictException; import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; -import org.zalando.nakadi.plugin.api.ApplicationService; import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.service.subscription.SubscriptionService; @@ -40,18 +37,13 @@ @RestController public class PostSubscriptionController extends NakadiProblemControllerAdvice { - private static final Logger LOG = LoggerFactory.getLogger(PostSubscriptionController.class); - private final FeatureToggleService featureToggleService; - private final ApplicationService applicationService; private final SubscriptionService subscriptionService; @Autowired public PostSubscriptionController(final FeatureToggleService featureToggleService, - final ApplicationService applicationService, final SubscriptionService subscriptionService) { this.featureToggleService = featureToggleService; - this.applicationService = applicationService; this.subscriptionService = subscriptionService; } diff --git a/src/main/java/org/zalando/nakadi/controller/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index 2d4923005e..87e24cc265 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -1,7 +1,5 @@ package org.zalando.nakadi.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -27,7 +25,6 @@ @RequestMapping(value = "/settings") public class SettingsController extends NakadiProblemControllerAdvice { - private static final Logger LOG = LoggerFactory.getLogger(SettingsController.class); private final BlacklistService blacklistService; private final FeatureToggleService featureToggleService; private final SecuritySettings securitySettings; diff --git a/src/main/java/org/zalando/nakadi/controller/StoragesController.java b/src/main/java/org/zalando/nakadi/controller/StoragesController.java index 08b7a39539..a90aadd39f 100644 --- a/src/main/java/org/zalando/nakadi/controller/StoragesController.java +++ b/src/main/java/org/zalando/nakadi/controller/StoragesController.java @@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.NativeWebRequest; -import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.domain.Storage; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; @@ -27,14 +26,11 @@ @RestController public class StoragesController { - private final SecuritySettings securitySettings; private final StorageService storageService; private final AdminService adminService; @Autowired - public StoragesController(final SecuritySettings securitySettings, final StorageService storageService, - final AdminService adminService) { - this.securitySettings = securitySettings; + public StoragesController(final StorageService storageService, final AdminService adminService) { this.storageService = storageService; this.adminService = adminService; } diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java index ec50774f87..23ea534681 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java @@ -1,7 +1,5 @@ package org.zalando.nakadi.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -16,7 +14,6 @@ import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; -import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.service.subscription.SubscriptionService; import org.zalando.nakadi.service.subscription.SubscriptionService.StatsMode; @@ -32,15 +29,10 @@ @RequestMapping(value = "/subscriptions") public class SubscriptionController extends NakadiProblemControllerAdvice { - private static final Logger LOG = LoggerFactory.getLogger(SubscriptionController.class); - - private final FeatureToggleService featureToggleService; private final SubscriptionService subscriptionService; @Autowired - public SubscriptionController(final FeatureToggleService featureToggleService, - final SubscriptionService subscriptionService) { - this.featureToggleService = featureToggleService; + public SubscriptionController(final SubscriptionService subscriptionService) { this.subscriptionService = subscriptionService; } diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java index 20969a7940..890ecc2887 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java @@ -28,7 +28,6 @@ import org.zalando.nakadi.security.Client; import org.zalando.nakadi.service.BlacklistService; import org.zalando.nakadi.service.ClosedConnectionsCrutch; -import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.service.subscription.StreamParameters; import org.zalando.nakadi.service.subscription.SubscriptionOutput; import org.zalando.nakadi.service.subscription.SubscriptionStreamer; @@ -60,7 +59,6 @@ public class SubscriptionStreamController extends NakadiProblemControllerAdvice private static final Logger LOG = LoggerFactory.getLogger(SubscriptionStreamController.class); private final SubscriptionStreamerFactory subscriptionStreamerFactory; - private final FeatureToggleService featureToggleService; private final ObjectMapper jsonMapper; private final ClosedConnectionsCrutch closedConnectionsCrutch; private final NakadiSettings nakadiSettings; @@ -71,7 +69,6 @@ public class SubscriptionStreamController extends NakadiProblemControllerAdvice @Autowired public SubscriptionStreamController(final SubscriptionStreamerFactory subscriptionStreamerFactory, - final FeatureToggleService featureToggleService, final ObjectMapper objectMapper, final ClosedConnectionsCrutch closedConnectionsCrutch, final NakadiSettings nakadiSettings, @@ -80,7 +77,6 @@ public SubscriptionStreamController(final SubscriptionStreamerFactory subscripti final SubscriptionDbRepository subscriptionDbRepository, final SubscriptionValidationService subscriptionValidationService) { this.subscriptionStreamerFactory = subscriptionStreamerFactory; - this.featureToggleService = featureToggleService; this.jsonMapper = objectMapper; this.closedConnectionsCrutch = closedConnectionsCrutch; this.nakadiSettings = nakadiSettings; diff --git a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java index 28a62e3af9..546352a60d 100644 --- a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java +++ b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java @@ -1,7 +1,5 @@ package org.zalando.nakadi.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -27,8 +25,6 @@ @RequestMapping(value = "/event-types/{name}/timelines", produces = APPLICATION_JSON_VALUE) public class TimelinesController extends NakadiProblemControllerAdvice { - private static final Logger LOG = LoggerFactory.getLogger(TimelinesController.class); - private final TimelineService timelineService; @Autowired diff --git a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java index 6db7fc6a2d..520a8ec8b0 100644 --- a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java @@ -94,8 +94,7 @@ public CursorsControllerTest() throws Exception { final CursorTokenService tokenService = mock(CursorTokenService.class); when(tokenService.generateToken()).thenReturn(TOKEN); - final CursorsController controller = new CursorsController(cursorsService, featureToggleService, - cursorConverter, tokenService); + final CursorsController controller = new CursorsController(cursorsService, cursorConverter, tokenService); final SecuritySettings settings = mock(SecuritySettings.class); doReturn(SecuritySettings.AuthMode.OFF).when(settings).getAuthMode(); diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java index 8187ee959d..53b274184d 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java @@ -110,7 +110,7 @@ public void init() throws Exception { featureToggleService, authorizationValidator, timelineSync, transactionTemplate, nakadiSettings, nakadiKpiPublisher, "et-log-event-type", eventTypeOptionsValidator, adminService); final EventTypeController controller = new EventTypeController(eventTypeService,featureToggleService, - applicationService, adminService, nakadiSettings); + adminService, nakadiSettings); doReturn(randomUUID).when(uuid).randomUUID(); doReturn(true).when(applicationService).exists(any()); diff --git a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java index b315432b94..9cbda1590b 100644 --- a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java @@ -63,7 +63,7 @@ public PostSubscriptionControllerTest() throws Exception { when(subscriptionService.getSubscriptionUri(any())).thenCallRealMethod(); final PostSubscriptionController controller = new PostSubscriptionController(featureToggleService, - applicationService, subscriptionService); + subscriptionService); mockMvcBuilder = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) diff --git a/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java b/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java index 48cae8d611..2c6a1cfb40 100644 --- a/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java @@ -41,7 +41,7 @@ public class StoragesControllerTest { @Before public void before() { - final StoragesController controller = new StoragesController(securitySettings, storageService, adminService); + final StoragesController controller = new StoragesController(storageService, adminService); final FeatureToggleService featureToggleService = mock(FeatureToggleService.class); doReturn("nakadi").when(securitySettings).getAdminClientId(); diff --git a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java index ac06ae8544..2381b42983 100644 --- a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java @@ -124,7 +124,7 @@ public SubscriptionControllerTest() throws Exception { zkSubscriptionClientFactory, timelineService, eventTypeRepository, null, cursorConverter, cursorOperationsService, nakadiKpiPublisher, featureToggleService, null, "subscription_log_et", mock(AuthorizationValidator.class)); - final SubscriptionController controller = new SubscriptionController(featureToggleService, subscriptionService); + final SubscriptionController controller = new SubscriptionController(subscriptionService); final ApplicationService applicationService = mock(ApplicationService.class); doReturn(true).when(applicationService).exists(any()); From e0991d08bc9582d7b31f1d2f0d1113c257e659cf Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 30 Aug 2018 16:38:29 +0200 Subject: [PATCH 03/23] gradle 4.10 --- gradle/wrapper/gradle-wrapper.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7dc503f149..786e104107 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Thu Aug 30 16:32:45 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip From 746642612308f619a1af672b19a61f2e28ca2b22 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 30 Aug 2018 16:47:59 +0200 Subject: [PATCH 04/23] Build file ready for Gradle 5.0 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 1ab1cf0632..a03ae8f200 100644 --- a/build.gradle +++ b/build.gradle @@ -188,8 +188,8 @@ dependencies { // end::dependencies[] // tag::wrapper[] -task wrapper(type: Wrapper) { - gradleVersion = '2.3' +wrapper { + gradleVersion = '4.10' } tasks.withType(FindBugs) { @@ -265,7 +265,7 @@ task fullAcceptanceTest(type: GradleBuild) { } task acceptanceTest(type: Test) { - testClassesDir = sourceSets.acceptanceTest.output.classesDir + testClassesDirs = sourceSets.acceptanceTest.output classpath = sourceSets.acceptanceTest.runtimeClasspath maxParallelForks = Runtime.runtime.availableProcessors() } From 48d3919827547fd517096300d8739520a60f901d Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 30 Aug 2018 17:36:06 +0200 Subject: [PATCH 05/23] Fix tests --- .../zalando/nakadi/controller/SettingsController.java | 10 +++++++++- .../org/zalando/nakadi/util/GzipBodyRequestFilter.java | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/controller/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index 87e24cc265..860d87a371 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -3,15 +3,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.ResourceAuthorization; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; +import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.security.Client; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.BlacklistService; @@ -97,10 +100,15 @@ public ResponseEntity getAdmins() { } @RequestMapping(path = "/admins", method = RequestMethod.POST) - public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthorization authz) { + public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthorization authz, + final Errors errors, + final NativeWebRequest request) { if (!adminService.isAdmin(AuthorizationService.Operation.ADMIN)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } + if (errors.hasErrors()) { + return create(new ValidationProblem(errors), request); + } adminService.updateAdmins(authz.toPermissionsList(ADMIN_RESOURCE)); return ResponseEntity.ok().build(); } diff --git a/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java b/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java index 28b83a8783..3bd26c5674 100644 --- a/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java +++ b/src/main/java/org/zalando/nakadi/util/GzipBodyRequestFilter.java @@ -49,7 +49,7 @@ public final void doFilter(final ServletRequest servletRequest, final ServletRes .map(encoding -> encoding.contains("gzip")) .orElse(false); - if (isGzipped && !POST.equals(request.getMethod())) { + if (isGzipped && !POST.matches(request.getMethod())) { reportNotAcceptableError((HttpServletResponse) servletResponse, request); return; } From 7e93e914fffc1732d5c906ff058d04541f5f31f8 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Tue, 4 Sep 2018 15:40:08 +0200 Subject: [PATCH 06/23] Post-merge fix --- .../nakadi/config/SecurityConfiguration.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java index 2270ca8ee4..e906f79c5f 100644 --- a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java +++ b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java @@ -19,16 +19,10 @@ import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; -import org.springframework.security.web.firewall.FirewalledRequest; -import org.springframework.security.web.firewall.HttpFirewall; -import org.springframework.security.web.firewall.RequestRejectedException; -import org.springframework.security.web.firewall.StrictHttpFirewall; import org.zalando.problem.Status; import org.zalando.problem.StatusType; import org.zalando.stups.oauth2.spring.security.expression.ExtendedOAuth2WebSecurityExpressionHandler; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Response; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -198,4 +192,13 @@ public String getDetail() { } } + private static Status fromStatusCode(final int code) { + for (final Status status: Status.values()) { + if (status.getStatusCode() == code) { + return status; + } + } + return null; + } + } From a52724c205ae7518039fd6328dd9f5a8ec823040 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Mon, 8 Oct 2018 16:55:46 +0200 Subject: [PATCH 07/23] Refactor --- .../CursorOperationsController.java | 30 +-- .../nakadi/controller/CursorsController.java | 89 ++------ .../controller/EventPublishingController.java | 47 ++-- .../controller/EventTypeController.java | 13 +- .../NakadiProblemControllerAdvice.java | 7 - .../controller/PartitionsController.java | 131 +++++------ .../PostSubscriptionController.java | 36 ++- .../nakadi/controller/SettingsController.java | 8 +- .../controller/SubscriptionController.java | 2 +- .../SubscriptionStreamController.java | 2 +- .../controller/TimelinesController.java | 2 +- .../advice/CursorOperationsHandler.java | 43 ++++ .../controller/advice/CursorsHandler.java | 50 +++++ .../advice/EventPublishingHandler.java | 38 ++++ .../{ => advice}/NakadiProblemHandling.java | 206 ++++++++++-------- .../advice/PostSubscriptionHandler.java | 35 +++ .../exceptions/runtime/BlockedException.java | 8 + ...SubscriptionCreationDisabledException.java | 8 + .../runtime/ValidationException.java | 17 ++ .../controller/CursorsControllerTest.java | 3 + .../EventPublishingControllerTest.java | 5 +- .../EventTypeControllerTestCase.java | 3 +- .../controller/NakadiProblemHandlingTest.java | 3 +- .../controller/PartitionsControllerTest.java | 8 +- .../PostSubscriptionControllerTest.java | 15 +- .../SubscriptionControllerTest.java | 25 ++- .../controller/TimelinesControllerTest.java | 3 +- 27 files changed, 483 insertions(+), 354 deletions(-) delete mode 100644 src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java rename src/main/java/org/zalando/nakadi/controller/{ => advice}/NakadiProblemHandling.java (72%) create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java create mode 100644 src/main/java/org/zalando/nakadi/exceptions/runtime/BlockedException.java create mode 100644 src/main/java/org/zalando/nakadi/exceptions/runtime/SubscriptionCreationDisabledException.java create mode 100644 src/main/java/org/zalando/nakadi/exceptions/runtime/ValidationException.java diff --git a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java index f5218c4d77..d72fe6c77a 100644 --- a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java +++ b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java @@ -4,13 +4,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.NakadiCursor; import org.zalando.nakadi.domain.NakadiCursorLag; @@ -18,7 +16,6 @@ import org.zalando.nakadi.exceptions.runtime.CursorConversionException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidCursorException; -import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NotFoundException; @@ -32,7 +29,6 @@ import org.zalando.nakadi.view.CursorDistance; import org.zalando.nakadi.view.CursorLag; import org.zalando.nakadi.view.ShiftedCursor; -import org.zalando.problem.Problem; import javax.validation.Valid; import java.util.List; @@ -41,10 +37,9 @@ import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.ResponseEntity.status; -import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class CursorOperationsController extends NakadiProblemControllerAdvice { +public class CursorOperationsController { private static final Logger LOG = LoggerFactory.getLogger(CursorOperationsController.class); @@ -131,29 +126,6 @@ public List cursorsLag(@PathVariable("eventTypeName") final String ev .collect(Collectors.toList()); } - @Override - @ExceptionHandler(InvalidCursorOperation.class) - public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, - final NativeWebRequest request) { - LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, - clientErrorMessage(exception.getReason())), request); - } - - private String clientErrorMessage(final InvalidCursorOperation.Reason reason) { - switch (reason) { - case TIMELINE_NOT_FOUND: return "Timeline not found. It might happen in case the cursor refers to a " + - "timeline that has already expired."; - case PARTITION_NOT_FOUND: return "Partition not found."; - case CURSOR_FORMAT_EXCEPTION: return "Сursor format is not supported."; - case CURSORS_WITH_DIFFERENT_PARTITION: return "Cursors with different partition. Pairs of cursors should " + - "have matching partitions."; - default: - LOG.error("Unexpected invalid cursor operation reason " + reason); - throw new NakadiBaseException(); - } - } - private CursorLag toCursorLag(final NakadiCursorLag nakadiCursorLag) { return new CursorLag( nakadiCursorLag.getPartition(), diff --git a/src/main/java/org/zalando/nakadi/controller/CursorsController.java b/src/main/java/org/zalando/nakadi/controller/CursorsController.java index a40e61d796..b2f5f24701 100644 --- a/src/main/java/org/zalando/nakadi/controller/CursorsController.java +++ b/src/main/java/org/zalando/nakadi/controller/CursorsController.java @@ -4,8 +4,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -22,15 +20,12 @@ import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; -import org.zalando.nakadi.exceptions.runtime.UnableProcessException; -import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.service.CursorConverter; import org.zalando.nakadi.service.CursorTokenService; import org.zalando.nakadi.service.CursorsService; import org.zalando.nakadi.view.CursorCommitResult; import org.zalando.nakadi.view.SubscriptionCursor; import org.zalando.nakadi.view.SubscriptionCursorWithoutToken; -import org.zalando.problem.Problem; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -41,13 +36,9 @@ import static org.springframework.http.ResponseEntity.noContent; import static org.springframework.http.ResponseEntity.ok; -import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; -import static org.zalando.problem.Status.NOT_FOUND; -import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; -import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class CursorsController extends NakadiProblemControllerAdvice { +public class CursorsController { private static final Logger LOG = LoggerFactory.getLogger(CursorsController.class); @@ -81,35 +72,26 @@ public ItemsWrapper getCursors(@PathVariable("subscriptionId public ResponseEntity commitCursors(@PathVariable("subscriptionId") final String subscriptionId, @Valid @RequestBody final ItemsWrapper cursorsIn, @NotNull @RequestHeader("X-Nakadi-StreamId") final String streamId, - final NativeWebRequest request) { - - try { - final List cursors = convertToNakadiCursors(cursorsIn); - if (cursors.isEmpty()) { - throw new CursorsAreEmptyException(); - } - final List items = cursorsService.commitCursors(streamId, subscriptionId, cursors); + final NativeWebRequest request) + throws NoSuchEventTypeException, + NoSuchSubscriptionException, + InvalidCursorException, + ServiceTemporarilyUnavailableException, + InternalNakadiException { + final List cursors = convertToNakadiCursors(cursorsIn); + if (cursors.isEmpty()) { + throw new CursorsAreEmptyException(); + } + final List items = cursorsService.commitCursors(streamId, subscriptionId, cursors); - final boolean allCommited = items.stream().allMatch(item -> item); - if (allCommited) { - return noContent().build(); - } else { - final List body = IntStream.range(0, cursorsIn.getItems().size()) - .mapToObj(idx -> new CursorCommitResult(cursorsIn.getItems().get(idx), items.get(idx))) - .collect(Collectors.toList()); - return ok(new ItemsWrapper<>(body)); - } - } catch (final NoSuchEventTypeException | InvalidCursorException e) { - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, e.getMessage()), request); - } catch (final NoSuchSubscriptionException e) { - LOG.error("Subscription not found", e); - return create(Problem.valueOf(NOT_FOUND, e.getMessage()), request); - } catch (final ServiceTemporarilyUnavailableException e) { - LOG.error("Failed to commit cursors", e); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, e.getMessage()), request); - } catch (final InternalNakadiException e) { - LOG.error("Failed to commit cursors", e); - return create(Problem.valueOf(INTERNAL_SERVER_ERROR, e.getMessage()), request); + final boolean allCommited = items.stream().allMatch(item -> item); + if (allCommited) { + return noContent().build(); + } else { + final List body = IntStream.range(0, cursorsIn.getItems().size()) + .mapToObj(idx -> new CursorCommitResult(cursorsIn.getItems().get(idx), items.get(idx))) + .collect(Collectors.toList()); + return ok(new ItemsWrapper<>(body)); } } @@ -117,17 +99,10 @@ public ResponseEntity commitCursors(@PathVariable("subscriptionId") final Str public ResponseEntity resetCursors( @PathVariable("subscriptionId") final String subscriptionId, @Valid @RequestBody final ItemsWrapper cursors, - final NativeWebRequest request) { - try { - cursorsService.resetCursors(subscriptionId, convertToNakadiCursors(cursors)); - return noContent().build(); - } catch (final NoSuchEventTypeException e) { - throw new UnableProcessException(e.getMessage()); - } catch (final InvalidCursorException e) { - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, e.getMessage()), request); - } catch (final InternalNakadiException e) { - return create(Problem.valueOf(INTERNAL_SERVER_ERROR, e.getMessage()), request); - } + final NativeWebRequest request) + throws NoSuchEventTypeException, InvalidCursorException, InternalNakadiException { + cursorsService.resetCursors(subscriptionId, convertToNakadiCursors(cursors)); + return noContent().build(); } private List convertToNakadiCursors( @@ -140,20 +115,4 @@ private List convertToNakadiCursors( } return nakadiCursors; } - - @Override - @ExceptionHandler(UnableProcessException.class) - public ResponseEntity handleUnableProcessException(final UnableProcessException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, ex.getMessage()), request); - } - - @Override - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, - final NativeWebRequest request) { - LOG.debug(ex.getMessage(), ex); - return create(new ValidationProblem(ex.getBindingResult()), request); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java index 160585864c..720b57f321 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java @@ -17,6 +17,7 @@ import org.zalando.nakadi.domain.EventPublishResult; import org.zalando.nakadi.domain.EventPublishingStatus; import org.zalando.nakadi.exceptions.runtime.AccessDeniedException; +import org.zalando.nakadi.exceptions.runtime.BlockedException; import org.zalando.nakadi.exceptions.runtime.EventTypeTimeoutException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; @@ -27,22 +28,17 @@ import org.zalando.nakadi.service.BlacklistService; import org.zalando.nakadi.service.EventPublisher; import org.zalando.nakadi.service.NakadiKpiPublisher; -import org.zalando.problem.Problem; -import org.zalando.problem.ThrowableProblem; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.springframework.http.ResponseEntity.status; import static org.springframework.web.bind.annotation.RequestMethod.POST; -import static org.zalando.problem.Status.BAD_REQUEST; -import static org.zalando.problem.Status.FORBIDDEN; import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; import static org.zalando.problem.Status.NOT_FOUND; -import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; @RestController -public class EventPublishingController extends NakadiProblemControllerAdvice { +public class EventPublishingController { private static final Logger LOG = LoggerFactory.getLogger(EventPublishingController.class); @@ -70,20 +66,24 @@ public EventPublishingController(final EventPublisher publisher, public ResponseEntity postEvent(@PathVariable final String eventTypeName, @RequestBody final String eventsAsString, final NativeWebRequest request, - final Client client) throws AccessDeniedException { + final Client client) + throws AccessDeniedException, BlockedException, ServiceTemporarilyUnavailableException, + InternalNakadiException, EventTypeTimeoutException, NoSuchEventTypeException { LOG.trace("Received event {} for event type {}", eventsAsString, eventTypeName); final EventTypeMetrics eventTypeMetrics = eventTypeMetricRegistry.metricsFor(eventTypeName); - try { - if (blacklistService.isProductionBlocked(eventTypeName, client.getClientId())) { - return create( - Problem.valueOf(FORBIDDEN, "Application or event type is blocked"), request); - } + if (blacklistService.isProductionBlocked(eventTypeName, client.getClientId())) { + throw new BlockedException("Application or event type is blocked"); + } + try { final ResponseEntity response = postEventInternal( eventTypeName, eventsAsString, request, eventTypeMetrics, client); eventTypeMetrics.incrementResponseCount(response.getStatusCode().value()); return response; + } catch (final NoSuchEventTypeException exception) { + eventTypeMetrics.incrementResponseCount(NOT_FOUND.getStatusCode()); + throw exception; } catch (final RuntimeException ex) { eventTypeMetrics.incrementResponseCount(INTERNAL_SERVER_ERROR.getStatusCode()); throw ex; @@ -95,7 +95,8 @@ private ResponseEntity postEventInternal(final String eventTypeName, final NativeWebRequest nativeWebRequest, final EventTypeMetrics eventTypeMetrics, final Client client) - throws AccessDeniedException, ServiceTemporarilyUnavailableException { + throws AccessDeniedException, ServiceTemporarilyUnavailableException, InternalNakadiException, + EventTypeTimeoutException, NoSuchEventTypeException { final long startingNanos = System.nanoTime(); try { final EventPublishResult result = publisher.publish(eventsAsString, eventTypeName); @@ -109,16 +110,13 @@ private ResponseEntity postEventInternal(final String eventTypeName, return response(result); } catch (final JSONException e) { LOG.debug("Problem parsing event", e); - return processJSONException(e, nativeWebRequest); + throw e; } catch (final NoSuchEventTypeException e) { LOG.debug("Event type not found.", e.getMessage()); - return create(Problem.valueOf(NOT_FOUND, e.getMessage()), nativeWebRequest); + throw e; } catch (final EventTypeTimeoutException e) { LOG.debug("Failed to publish batch", e); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, e.getMessage()), nativeWebRequest); - } catch (final InternalNakadiException e) { - LOG.debug("Failed to publish batch", e); - return create(Problem.valueOf(INTERNAL_SERVER_ERROR, e.getMessage()), nativeWebRequest); + throw e; } finally { eventTypeMetrics.updateTiming(startingNanos, System.nanoTime()); } @@ -157,17 +155,6 @@ private void reportMetrics(final EventTypeMetrics eventTypeMetrics, final EventP } } - private ResponseEntity processJSONException(final JSONException e, final NativeWebRequest nativeWebRequest) { - if (e.getCause() == null) { - return create(createProblem(e), nativeWebRequest); - } - return create(Problem.valueOf(BAD_REQUEST), nativeWebRequest); - } - - private ThrowableProblem createProblem(final JSONException e) { - return Problem.valueOf(BAD_REQUEST, "Error occurred when parsing event(s). " + e.getMessage()); - } - private ResponseEntity response(final EventPublishResult result) { switch (result.getStatus()) { case SUBMITTED: diff --git a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java index bc1462a14e..2af0bd8bda 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java @@ -30,8 +30,8 @@ import org.zalando.nakadi.exceptions.runtime.TopicConfigException; import org.zalando.nakadi.exceptions.runtime.TopicCreationException; import org.zalando.nakadi.exceptions.runtime.UnableProcessException; +import org.zalando.nakadi.exceptions.runtime.ValidationException; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; -import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.EventTypeService; import org.zalando.nakadi.service.FeatureToggleService; @@ -46,7 +46,7 @@ @RestController @RequestMapping(value = "/event-types") -public class EventTypeController extends NakadiProblemControllerAdvice { +public class EventTypeController { private final EventTypeService eventTypeService; private final FeatureToggleService featureToggleService; @@ -76,13 +76,13 @@ public ResponseEntity create(@Valid @RequestBody final EventTypeBase eventTyp final Errors errors, final NativeWebRequest request) throws TopicCreationException, InternalNakadiException, NoSuchPartitionStrategyException, - DuplicatedEventTypeNameException, InvalidEventTypeException { + DuplicatedEventTypeNameException, InvalidEventTypeException, ValidationException { if (featureToggleService.isFeatureEnabled(DISABLE_EVENT_TYPE_CREATION)) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } if (errors.hasErrors()) { - return create(new ValidationProblem(errors), request); + throw new ValidationException(errors); } eventTypeService.create(eventType); @@ -118,9 +118,10 @@ public ResponseEntity update( NakadiRuntimeException, ServiceTemporarilyUnavailableException, UnableProcessException, - NoSuchPartitionStrategyException{ + NoSuchPartitionStrategyException, + ValidationException { if (errors.hasErrors()) { - return create(new ValidationProblem(errors), request); + throw new ValidationException(errors); } eventTypeService.update(name, eventType); diff --git a/src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java b/src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java deleted file mode 100644 index 0d83c8288b..0000000000 --- a/src/main/java/org/zalando/nakadi/controller/NakadiProblemControllerAdvice.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.zalando.nakadi.controller; - -import org.springframework.web.bind.annotation.ControllerAdvice; - -@ControllerAdvice -public class NakadiProblemControllerAdvice implements NakadiProblemHandling { -} diff --git a/src/main/java/org/zalando/nakadi/controller/PartitionsController.java b/src/main/java/org/zalando/nakadi/controller/PartitionsController.java index 53aebac950..7b607eb3f7 100644 --- a/src/main/java/org/zalando/nakadi/controller/PartitionsController.java +++ b/src/main/java/org/zalando/nakadi/controller/PartitionsController.java @@ -31,7 +31,6 @@ import org.zalando.nakadi.view.Cursor; import org.zalando.nakadi.view.CursorLag; import org.zalando.nakadi.view.EventTypePartitionView; -import org.zalando.problem.Problem; import javax.annotation.Nullable; import java.util.Collections; @@ -40,12 +39,9 @@ import java.util.stream.Collectors; import static org.springframework.http.ResponseEntity.ok; -import static org.zalando.problem.Status.NOT_FOUND; -import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; -import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class PartitionsController extends NakadiProblemControllerAdvice { +public class PartitionsController { private static final Logger LOG = LoggerFactory.getLogger(PartitionsController.class); @@ -68,41 +64,54 @@ public PartitionsController(final TimelineService timelineService, this.authorizationValidator = authorizationValidator; } + private static NakadiCursor selectLast(final List activeTimelines, final PartitionEndStatistics last, + final PartitionStatistics first) { + final NakadiCursor lastInLastTimeline = last.getLast(); + if (!lastInLastTimeline.isInitial()) { + return lastInLastTimeline; + } + // There may be a situation, when there is no data in all the timelines after first, but any cursor from the + // next after first timelines is greater then the cursor in first timeline. Therefore we need to roll pointer + // to the end back till the very beginning or to the end of first timeline with data. + for (int idx = activeTimelines.size() - 2; idx > 0; --idx) { + final Timeline timeline = activeTimelines.get(idx); + final NakadiCursor lastInTimeline = timeline.getLatestPosition() + .toNakadiCursor(timeline, first.getPartition()); + if (!lastInTimeline.isInitial()) { + return lastInTimeline; + } + } + return first.getLast(); + } + @RequestMapping(value = "/event-types/{name}/partitions", method = RequestMethod.GET) public ResponseEntity listPartitions(@PathVariable("name") final String eventTypeName, - final NativeWebRequest request) { + final NativeWebRequest request) throws NoSuchEventTypeException { LOG.trace("Get partitions endpoint for event-type '{}' is called", eventTypeName); - try { - final EventType eventType = eventTypeRepository.findByName(eventTypeName); - authorizationValidator.authorizeStreamRead(eventType); - - final List timelines = timelineService.getActiveTimelinesOrdered(eventTypeName); - final List firstStats = timelineService.getTopicRepository(timelines.get(0)) - .loadTopicStatistics(Collections.singletonList(timelines.get(0))); - final List lastStats; - if (timelines.size() == 1) { - lastStats = firstStats; - } else { - lastStats = timelineService.getTopicRepository(timelines.get(timelines.size() - 1)) - .loadTopicStatistics(Collections.singletonList(timelines.get(timelines.size() - 1))); - } - final List result = firstStats.stream().map(first -> { - final PartitionStatistics last = lastStats.stream() - .filter(l -> l.getPartition().equals(first.getPartition())) - .findAny().get(); - return new EventTypePartitionView( - eventTypeName, - first.getPartition(), - cursorConverter.convert(first.getFirst()).getOffset(), - cursorConverter.convert(selectLast(timelines, last, first)).getOffset()); - }).collect(Collectors.toList()); - return ok().body(result); - } catch (final NoSuchEventTypeException e) { - return create(Problem.valueOf(NOT_FOUND, "topic not found"), request); - } catch (final InternalNakadiException e) { - LOG.error("Could not list partitions. Respond with SERVICE_UNAVAILABLE.", e); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, e.getMessage()), request); + final EventType eventType = eventTypeRepository.findByName(eventTypeName); + authorizationValidator.authorizeStreamRead(eventType); + + final List timelines = timelineService.getActiveTimelinesOrdered(eventTypeName); + final List firstStats = timelineService.getTopicRepository(timelines.get(0)) + .loadTopicStatistics(Collections.singletonList(timelines.get(0))); + final List lastStats; + if (timelines.size() == 1) { + lastStats = firstStats; + } else { + lastStats = timelineService.getTopicRepository(timelines.get(timelines.size() - 1)) + .loadTopicStatistics(Collections.singletonList(timelines.get(timelines.size() - 1))); } + final List result = firstStats.stream().map(first -> { + final PartitionStatistics last = lastStats.stream() + .filter(l -> l.getPartition().equals(first.getPartition())) + .findAny().get(); + return new EventTypePartitionView( + eventTypeName, + first.getPartition(), + cursorConverter.convert(first.getFirst()).getOffset(), + cursorConverter.convert(selectLast(timelines, last, first)).getOffset()); + }).collect(Collectors.toList()); + return ok().body(result); } @RequestMapping(value = "/event-types/{name}/partitions/{partition}", method = RequestMethod.GET) @@ -110,29 +119,20 @@ public ResponseEntity getPartition( @PathVariable("name") final String eventTypeName, @PathVariable("partition") final String partition, @Nullable @RequestParam(value = "consumed_offset", required = false) final String consumedOffset, - final NativeWebRequest request) { + final NativeWebRequest request) throws NoSuchEventTypeException { LOG.trace("Get partition endpoint for event-type '{}', partition '{}' is called", eventTypeName, partition); - try { - final EventType eventType = eventTypeRepository.findByName(eventTypeName); - authorizationValidator.authorizeStreamRead(eventType); + final EventType eventType = eventTypeRepository.findByName(eventTypeName); + authorizationValidator.authorizeStreamRead(eventType); - if (consumedOffset != null) { - final CursorLag cursorLag = getCursorLag(eventTypeName, partition, consumedOffset); - return ok().body(cursorLag); - } else { - final EventTypePartitionView result = getTopicPartition(eventTypeName, partition); + if (consumedOffset != null) { + final CursorLag cursorLag = getCursorLag(eventTypeName, partition, consumedOffset); + return ok().body(cursorLag); + } else { + final EventTypePartitionView result = getTopicPartition(eventTypeName, partition); - return ok().body(result); - } - } catch (final NoSuchEventTypeException e) { - return create(Problem.valueOf(NOT_FOUND, "topic not found"), request); - } catch (final InternalNakadiException e) { - LOG.error("Could not get partition. Respond with SERVICE_UNAVAILABLE.", e); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, e.getMessage()), request); - } catch (final InvalidCursorException e) { - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), - request); + return ok().body(result); } + } private CursorLag getCursorLag(final String eventTypeName, final String partition, final String consumedOffset) @@ -182,25 +182,4 @@ private CursorLag toCursorLag(final NakadiCursorLag nakadiCursorLag) { ); } - - private static NakadiCursor selectLast(final List activeTimelines, final PartitionEndStatistics last, - final PartitionStatistics first) { - final NakadiCursor lastInLastTimeline = last.getLast(); - if (!lastInLastTimeline.isInitial()) { - return lastInLastTimeline; - } - // There may be a situation, when there is no data in all the timelines after first, but any cursor from the - // next after first timelines is greater then the cursor in first timeline. Therefore we need to roll pointer - // to the end back till the very beginning or to the end of first timeline with data. - for (int idx = activeTimelines.size() - 2; idx > 0; --idx) { - final Timeline timeline = activeTimelines.get(idx); - final NakadiCursor lastInTimeline = timeline.getLatestPosition() - .toNakadiCursor(timeline, first.getPartition()); - if (!lastInTimeline.isInitial()) { - return lastInTimeline; - } - } - return first.getLast(); - } - } diff --git a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java index 51df4938ab..baf663e3f9 100644 --- a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java @@ -17,25 +17,22 @@ import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; +import org.zalando.nakadi.exceptions.runtime.SubscriptionCreationDisabledException; import org.zalando.nakadi.exceptions.runtime.SubscriptionUpdateConflictException; import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; -import org.zalando.nakadi.problem.ValidationProblem; +import org.zalando.nakadi.exceptions.runtime.ValidationException; import org.zalando.nakadi.service.FeatureToggleService; import org.zalando.nakadi.service.subscription.SubscriptionService; -import org.zalando.problem.Problem; import javax.validation.Valid; import static org.apache.http.HttpHeaders.CONTENT_LOCATION; import static org.springframework.http.HttpStatus.OK; import static org.zalando.nakadi.service.FeatureToggleService.Feature.DISABLE_SUBSCRIPTION_CREATION; -import static org.zalando.problem.Status.NOT_FOUND; -import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; -import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class PostSubscriptionController extends NakadiProblemControllerAdvice { +public class PostSubscriptionController { private final FeatureToggleService featureToggleService; private final SubscriptionService subscriptionService; @@ -50,17 +47,20 @@ public PostSubscriptionController(final FeatureToggleService featureToggleServic @RequestMapping(value = "/subscriptions", method = RequestMethod.POST) public ResponseEntity createOrGetSubscription(@Valid @RequestBody final SubscriptionBase subscriptionBase, final Errors errors, - final NativeWebRequest request) { + final NativeWebRequest request) + throws ValidationException, + UnprocessableSubscriptionException, + InconsistentStateException, + SubscriptionCreationDisabledException { if (errors.hasErrors()) { - return create(new ValidationProblem(errors), request); + throw new ValidationException(errors); } try { return ok(subscriptionService.getExistingSubscription(subscriptionBase)); } catch (final NoSuchSubscriptionException e) { if (featureToggleService.isFeatureEnabled(DISABLE_SUBSCRIPTION_CREATION)) { - return create(Problem.valueOf(SERVICE_UNAVAILABLE, - "Subscription creation is temporarily unavailable"), request); + throw new SubscriptionCreationDisabledException("Subscription creation is temporarily unavailable"); } try { final Subscription subscription = subscriptionService.createSubscription(subscriptionBase); @@ -78,18 +78,14 @@ public ResponseEntity updateSubscription( @PathVariable("subscription_id") final String subscriptionId, @Valid @RequestBody final SubscriptionBase subscription, final Errors errors, - final NativeWebRequest request) { + final NativeWebRequest request) + throws NoSuchSubscriptionException, ValidationException, SubscriptionUpdateConflictException { if (errors.hasErrors()) { - return create(new ValidationProblem(errors), request); - } - try { - subscriptionService.updateSubscription(subscriptionId, subscription); - return ResponseEntity.noContent().build(); - } catch (final SubscriptionUpdateConflictException ex) { - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, ex.getMessage()), request); - } catch (final NoSuchSubscriptionException ex) { - return create(Problem.valueOf(NOT_FOUND, ex.getMessage()), request); + throw new ValidationException(errors); } + subscriptionService.updateSubscription(subscriptionId, subscription); + return ResponseEntity.noContent().build(); + } private ResponseEntity ok(final Subscription existingSubscription) { diff --git a/src/main/java/org/zalando/nakadi/controller/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index 860d87a371..f1520fabf9 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -13,8 +13,8 @@ import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.ResourceAuthorization; +import org.zalando.nakadi.exceptions.runtime.ValidationException; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; -import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.nakadi.security.Client; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.BlacklistService; @@ -26,7 +26,7 @@ @RestController @RequestMapping(value = "/settings") -public class SettingsController extends NakadiProblemControllerAdvice { +public class SettingsController { private final BlacklistService blacklistService; private final FeatureToggleService featureToggleService; @@ -102,12 +102,12 @@ public ResponseEntity getAdmins() { @RequestMapping(path = "/admins", method = RequestMethod.POST) public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthorization authz, final Errors errors, - final NativeWebRequest request) { + final NativeWebRequest request) throws ValidationException { if (!adminService.isAdmin(AuthorizationService.Operation.ADMIN)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } if (errors.hasErrors()) { - return create(new ValidationProblem(errors), request); + throw new ValidationException(errors); } adminService.updateAdmins(authz.toPermissionsList(ADMIN_RESOURCE)); return ResponseEntity.ok().build(); diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java index 23ea534681..8aad5cc2ee 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java @@ -27,7 +27,7 @@ @RestController @RequestMapping(value = "/subscriptions") -public class SubscriptionController extends NakadiProblemControllerAdvice { +public class SubscriptionController { private final SubscriptionService subscriptionService; diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java index 890ecc2887..506f5541a4 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionStreamController.java @@ -54,7 +54,7 @@ import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @RestController -public class SubscriptionStreamController extends NakadiProblemControllerAdvice { +public class SubscriptionStreamController { public static final String CONSUMERS_COUNT_METRIC_NAME = "consumers"; private static final Logger LOG = LoggerFactory.getLogger(SubscriptionStreamController.class); diff --git a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java index 546352a60d..179ca52561 100644 --- a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java +++ b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java @@ -23,7 +23,7 @@ @RestController @RequestMapping(value = "/event-types/{name}/timelines", produces = APPLICATION_JSON_VALUE) -public class TimelinesController extends NakadiProblemControllerAdvice { +public class TimelinesController { private final TimelineService timelineService; diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java new file mode 100644 index 0000000000..e4d303d006 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java @@ -0,0 +1,43 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.CursorOperationsController; +import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; +import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = CursorOperationsController.class) +public class CursorOperationsHandler implements AdviceTrait { + + + @ExceptionHandler(InvalidCursorOperation.class) + public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, + final NativeWebRequest request) { + LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, + clientErrorMessage(exception.getReason())), request); + } + + private String clientErrorMessage(final InvalidCursorOperation.Reason reason) { + switch (reason) { + case TIMELINE_NOT_FOUND: return "Timeline not found. It might happen in case the cursor refers to a " + + "timeline that has already expired."; + case PARTITION_NOT_FOUND: return "Partition not found."; + case CURSOR_FORMAT_EXCEPTION: return "Сursor format is not supported."; + case CURSORS_WITH_DIFFERENT_PARTITION: return "Cursors with different partition. Pairs of cursors should " + + "have matching partitions."; + default: + LOG.error("Unexpected invalid cursor operation reason " + reason); + throw new NakadiBaseException(); + } + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java new file mode 100644 index 0000000000..6bbe287cdc --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java @@ -0,0 +1,50 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.CursorsController; +import org.zalando.nakadi.exceptions.runtime.InvalidCursorException; +import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; +import org.zalando.nakadi.exceptions.runtime.UnableProcessException; +import org.zalando.nakadi.problem.ValidationProblem; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = CursorsController.class) +public class CursorsHandler implements AdviceTrait { + + @ExceptionHandler(UnableProcessException.class) + public ResponseEntity handleUnableProcessException(final UnableProcessException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(new ValidationProblem(exception.getBindingResult()), request); + } + + @ExceptionHandler(NoSuchEventTypeException.class) + public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidCursorException.class) + public ResponseEntity handleInvalidCursorException(final InvalidCursorException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java new file mode 100644 index 0000000000..0b4241e211 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java @@ -0,0 +1,38 @@ +package org.zalando.nakadi.controller.advice; + +import org.json.JSONException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.EventPublishingController; +import org.zalando.nakadi.exceptions.runtime.EventTypeTimeoutException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.BAD_REQUEST; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; + +@Priority(10) +@ControllerAdvice(assignableTypes = EventPublishingController.class) +public class EventPublishingHandler implements AdviceTrait { + + @ExceptionHandler(EventTypeTimeoutException.class) + public ResponseEntity handleEventTypeTimeoutException(final EventTypeTimeoutException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(JSONException.class) + public ResponseEntity handleJSONException(final JSONException exception, + final NativeWebRequest request) { + if (exception.getCause() == null) { + return create(Problem.valueOf(BAD_REQUEST, + "Error occurred when parsing event(s). " + exception.getMessage()), request); + } + return create(Problem.valueOf(BAD_REQUEST), request); + } +} + diff --git a/src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java similarity index 72% rename from src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java rename to src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java index db9c8c012d..4a04139b9c 100644 --- a/src/main/java/org/zalando/nakadi/controller/NakadiProblemHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java @@ -1,4 +1,4 @@ -package org.zalando.nakadi.controller; +package org.zalando.nakadi.controller.advice; import com.fasterxml.jackson.databind.JsonMappingException; import com.google.common.base.CaseFormat; @@ -7,9 +7,11 @@ import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.exceptions.runtime.AccessDeniedException; +import org.zalando.nakadi.exceptions.runtime.BlockedException; import org.zalando.nakadi.exceptions.runtime.CompactionException; import org.zalando.nakadi.exceptions.runtime.ConflictException; import org.zalando.nakadi.exceptions.runtime.CursorConversionException; @@ -56,11 +58,15 @@ import org.zalando.nakadi.exceptions.runtime.UnknownStorageTypeException; import org.zalando.nakadi.exceptions.runtime.UnprocessableEntityException; import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; +import org.zalando.nakadi.exceptions.runtime.ValidationException; import org.zalando.nakadi.exceptions.runtime.WrongInitialCursorsException; import org.zalando.nakadi.exceptions.runtime.WrongStreamParametersException; +import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.problem.Problem; import org.zalando.problem.spring.web.advice.ProblemHandling; +import javax.annotation.Priority; + import static org.zalando.problem.Status.BAD_REQUEST; import static org.zalando.problem.Status.CONFLICT; import static org.zalando.problem.Status.FORBIDDEN; @@ -72,35 +78,35 @@ import static org.zalando.problem.Status.TOO_MANY_REQUESTS; import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; +@Priority(20) +@ControllerAdvice +public class NakadiProblemHandling implements ProblemHandling { -public interface NakadiProblemHandling extends ProblemHandling { - - Logger LOG = LoggerFactory.getLogger(NakadiProblemHandling.class); - - String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; + private static final Logger LOG = LoggerFactory.getLogger(NakadiProblemHandling.class); + static final String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; @Override - default String formatFieldName(final String fieldName) { + public String formatFieldName(final String fieldName) { return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName); } @Override @ExceptionHandler - default ResponseEntity handleThrowable(final Throwable throwable, final NativeWebRequest request) { + public ResponseEntity handleThrowable(final Throwable throwable, final NativeWebRequest request) { final String errorTraceId = generateErrorTraceId(); LOG.error("InternalServerError (" + errorTraceId + "):", throwable); return create(Problem.valueOf(INTERNAL_SERVER_ERROR, "An internal error happened. Please report it. (" + errorTraceId + ")"), request); } - default String generateErrorTraceId() { + public String generateErrorTraceId() { return "ETI" + RandomStringUtils.randomAlphanumeric(24); } @Override @ExceptionHandler - default ResponseEntity handleMessageNotReadableException(final HttpMessageNotReadableException exception, - final NativeWebRequest request) { + public ResponseEntity handleMessageNotReadableException(final HttpMessageNotReadableException exception, + final NativeWebRequest request) { /* Unwrap nested JsonMappingException because the enclosing HttpMessageNotReadableException adds some ugly, Java class and stacktrace like information. @@ -116,41 +122,48 @@ class and stacktrace like information. } @ExceptionHandler(AccessDeniedException.class) - default ResponseEntity handleAccessDeniedException(final AccessDeniedException exception, - final NativeWebRequest request) { + public ResponseEntity handleAccessDeniedException(final AccessDeniedException exception, + final NativeWebRequest request) { return create(Problem.valueOf(FORBIDDEN, exception.explain()), request); } + @ExceptionHandler(BlockedException.class) + public ResponseEntity handleBlockedException(final BlockedException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); + } + @ExceptionHandler(CompactionException.class) - default ResponseEntity handleCompactionException(final CompactionException exception, - final NativeWebRequest request) { + public ResponseEntity handleCompactionException(final CompactionException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(ConflictException.class) - default ResponseEntity handleConflictException(final ConflictException exception, - final NativeWebRequest request) { + public ResponseEntity handleConflictException(final ConflictException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); } @ExceptionHandler(CursorsAreEmptyException.class) - default ResponseEntity handleCursorsAreEmptyException(final RuntimeException exception, - final NativeWebRequest request) { + public ResponseEntity handleCursorsAreEmptyException(final RuntimeException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(CursorConversionException.class) - default ResponseEntity handleCursorConversionException(final CursorConversionException exception, - final NativeWebRequest request) { + public ResponseEntity handleCursorConversionException(final CursorConversionException exception, + final NativeWebRequest request) { LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(DbWriteOperationsBlockedException.class) - default ResponseEntity handleDbWriteOperationsBlockedException( + public ResponseEntity handleDbWriteOperationsBlockedException( final DbWriteOperationsBlockedException exception, final NativeWebRequest request) { LOG.warn(exception.getMessage()); return create(Problem.valueOf(SERVICE_UNAVAILABLE, @@ -158,7 +171,7 @@ default ResponseEntity handleDbWriteOperationsBlockedException( } @ExceptionHandler(DuplicatedEventTypeNameException.class) - default ResponseEntity handleDuplicatedEventTypeNameException( + public ResponseEntity handleDuplicatedEventTypeNameException( final DuplicatedEventTypeNameException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); @@ -166,7 +179,7 @@ default ResponseEntity handleDuplicatedEventTypeNameException( } @ExceptionHandler(DuplicatedStorageException.class) - default ResponseEntity handleDuplicatedStorageException( + public ResponseEntity handleDuplicatedStorageException( final DuplicatedStorageException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -174,14 +187,14 @@ default ResponseEntity handleDuplicatedStorageException( } @ExceptionHandler(EnrichmentException.class) - default ResponseEntity handleEnrichmentException(final EnrichmentException exception, - final NativeWebRequest request) { + public ResponseEntity handleEnrichmentException(final EnrichmentException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(ErrorGettingCursorTimeLagException.class) - default ResponseEntity handleErrorGettingCursorTimeLagException( + public ResponseEntity handleErrorGettingCursorTimeLagException( final ErrorGettingCursorTimeLagException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); @@ -189,14 +202,14 @@ default ResponseEntity handleErrorGettingCursorTimeLagException( } @ExceptionHandler(EventTypeDeletionException.class) - default ResponseEntity handleEventTypeDeletionException(final EventTypeDeletionException exception, - final NativeWebRequest request) { + public ResponseEntity handleEventTypeDeletionException(final EventTypeDeletionException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } @ExceptionHandler(EventTypeOptionsValidationException.class) - default ResponseEntity handleEventTypeOptionsValidationException( + public ResponseEntity handleEventTypeOptionsValidationException( final EventTypeOptionsValidationException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); @@ -204,14 +217,14 @@ default ResponseEntity handleEventTypeOptionsValidationException( } @ExceptionHandler(EventTypeUnavailableException.class) - default ResponseEntity handleEventTypeUnavailableException(final EventTypeUnavailableException exception, - final NativeWebRequest request) { + public ResponseEntity handleEventTypeUnavailableException(final EventTypeUnavailableException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler(FeatureNotAvailableException.class) - default ResponseEntity handleFeatureNotAvailableException( + public ResponseEntity handleFeatureNotAvailableException( final FeatureNotAvailableException ex, final NativeWebRequest request) { LOG.debug(ex.getMessage()); @@ -219,34 +232,41 @@ default ResponseEntity handleFeatureNotAvailableException( } @ExceptionHandler(IllegalClientIdException.class) - default ResponseEntity handleIllegalClientIdException(final IllegalClientIdException exception, - final NativeWebRequest request) { + public ResponseEntity handleIllegalClientIdException(final IllegalClientIdException exception, + final NativeWebRequest request) { return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); } @ExceptionHandler(InconsistentStateException.class) - default ResponseEntity handleInconsistentStateExcetpion(final InconsistentStateException exception, - final NativeWebRequest request) { + public ResponseEntity handleInconsistentStateExcetpion(final InconsistentStateException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } + @ExceptionHandler(InternalNakadiException.class) + public ResponseEntity handleInternalNakadiException(final InternalNakadiException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + @ExceptionHandler(InvalidCursorOperation.class) - default ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, - final NativeWebRequest request) { + public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, + final NativeWebRequest request) { LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); } @ExceptionHandler(InvalidEventTypeException.class) - default ResponseEntity handleInvalidEventTypeException(final InvalidEventTypeException exception, - final NativeWebRequest request) { + public ResponseEntity handleInvalidEventTypeException(final InvalidEventTypeException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(InvalidLimitException.class) - default ResponseEntity handleInvalidLimitException( + public ResponseEntity handleInvalidLimitException( final InvalidLimitException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -254,7 +274,7 @@ default ResponseEntity handleInvalidLimitException( } @ExceptionHandler(InvalidPartitionKeyFieldsException.class) - default ResponseEntity handleInvalidPartitionKeyFieldsException( + public ResponseEntity handleInvalidPartitionKeyFieldsException( final InvalidPartitionKeyFieldsException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -262,14 +282,14 @@ default ResponseEntity handleInvalidPartitionKeyFieldsException( } @ExceptionHandler(InvalidStreamIdException.class) - default ResponseEntity handleInvalidStreamIdException(final InvalidStreamIdException exception, - final NativeWebRequest request) { + public ResponseEntity handleInvalidStreamIdException(final InvalidStreamIdException exception, + final NativeWebRequest request) { LOG.warn("Stream id {} is not found: {}", exception.getStreamId(), exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(InvalidVersionNumberException.class) - default ResponseEntity handleInvalidVersionNumberException( + public ResponseEntity handleInvalidVersionNumberException( final InvalidVersionNumberException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -277,22 +297,22 @@ default ResponseEntity handleInvalidVersionNumberException( } @ExceptionHandler(LimitReachedException.class) - default ResponseEntity handleLimitReachedException( + public ResponseEntity handleLimitReachedException( final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { LOG.warn(exception.getMessage()); return create(Problem.valueOf(TOO_MANY_REQUESTS, exception.getMessage()), request); } @ExceptionHandler(NakadiBaseException.class) - default ResponseEntity handleNakadiBaseException(final NakadiBaseException exception, - final NativeWebRequest request) { + public ResponseEntity handleNakadiBaseException(final NakadiBaseException exception, + final NativeWebRequest request) { LOG.error("Unexpected problem occurred", exception); return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } @ExceptionHandler - default ResponseEntity handleNakadiRuntimeException(final NakadiRuntimeException exception, - final NativeWebRequest request) throws Exception { + public ResponseEntity handleNakadiRuntimeException(final NakadiRuntimeException exception, + final NativeWebRequest request) throws Exception { final Throwable cause = exception.getCause(); if (cause instanceof InternalNakadiException) { return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); @@ -301,28 +321,28 @@ default ResponseEntity handleNakadiRuntimeException(final NakadiRuntime } @ExceptionHandler(NotFoundException.class) - default ResponseEntity handleNotFoundException(final NotFoundException exception, - final NativeWebRequest request) { + public ResponseEntity handleNotFoundException(final NotFoundException exception, + final NativeWebRequest request) { LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } @ExceptionHandler(NoStreamingSlotsAvailable.class) - default ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, - final NativeWebRequest request) { + public ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); } @ExceptionHandler(NoSuchEventTypeException.class) - default ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, - final NativeWebRequest request) { + public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } @ExceptionHandler(NoSuchPartitionStrategyException.class) - default ResponseEntity handleNoSuchPartitionStrategyException( + public ResponseEntity handleNoSuchPartitionStrategyException( final NoSuchPartitionStrategyException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -330,14 +350,14 @@ default ResponseEntity handleNoSuchPartitionStrategyException( } @ExceptionHandler(NoSuchSchemaException.class) - default ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, - final NativeWebRequest request) { + public ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } @ExceptionHandler(NoSuchStorageException.class) - default ResponseEntity handleNoSuchStorageException( + public ResponseEntity handleNoSuchStorageException( final NoSuchStorageException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -345,42 +365,42 @@ default ResponseEntity handleNoSuchStorageException( } @ExceptionHandler(NoSuchSubscriptionException.class) - default ResponseEntity handleNoSuchSubscriptionException(final NoSuchSubscriptionException exception, - final NativeWebRequest request) { + public ResponseEntity handleNoSuchSubscriptionException(final NoSuchSubscriptionException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } @ExceptionHandler(PartitioningException.class) - default ResponseEntity handlePartitioningException(final PartitioningException exception, - final NativeWebRequest request) { + public ResponseEntity handlePartitioningException(final PartitioningException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(RepositoryProblemException.class) - default ResponseEntity handleRepositoryProblemException(final RepositoryProblemException exception, - final NativeWebRequest request) { + public ResponseEntity handleRepositoryProblemException(final RepositoryProblemException exception, + final NativeWebRequest request) { LOG.error("Repository problem occurred", exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler(RequestInProgressException.class) - default ResponseEntity handleRequestInProgressException(final RequestInProgressException exception, - final NativeWebRequest request) { + public ResponseEntity handleRequestInProgressException(final RequestInProgressException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); } @ExceptionHandler(ServiceTemporarilyUnavailableException.class) - default ResponseEntity handleServiceTemporarilyUnavailableException( + public ResponseEntity handleServiceTemporarilyUnavailableException( final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler(StorageIsUsedException.class) - default ResponseEntity handleStorageIsUsedException( + public ResponseEntity handleStorageIsUsedException( final StorageIsUsedException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -388,15 +408,15 @@ default ResponseEntity handleStorageIsUsedException( } @ExceptionHandler(TimeLagStatsTimeoutException.class) - default ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException exception, - final NativeWebRequest request) { + public ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException exception, + final NativeWebRequest request) { LOG.warn(exception.getMessage()); return create(Problem.valueOf(REQUEST_TIMEOUT, exception.getMessage()), request); } @ExceptionHandler(TimelineException.class) - default ResponseEntity handleTimelineException(final TimelineException exception, - final NativeWebRequest request) { + public ResponseEntity handleTimelineException(final TimelineException exception, + final NativeWebRequest request) { LOG.error(exception.getMessage(), exception); final Throwable cause = exception.getCause(); if (cause instanceof InternalNakadiException) { @@ -406,35 +426,35 @@ default ResponseEntity handleTimelineException(final TimelineException } @ExceptionHandler(TooManyPartitionsException.class) - default ResponseEntity handleTooManyPartitionsException(final TooManyPartitionsException exception, - final NativeWebRequest request) { + public ResponseEntity handleTooManyPartitionsException(final TooManyPartitionsException exception, + final NativeWebRequest request) { LOG.debug("Error occurred when working with subscriptions", exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(TopicCreationException.class) - default ResponseEntity handleTopicCreationException(final TopicCreationException exception, - final NativeWebRequest request) { + public ResponseEntity handleTopicCreationException(final TopicCreationException exception, + final NativeWebRequest request) { LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler(UnableProcessException.class) - default ResponseEntity handleUnableProcessException(final UnableProcessException exception, - final NativeWebRequest request) { + public ResponseEntity handleUnableProcessException(final UnableProcessException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(UnknownOperationException.class) - default ResponseEntity handleUnknownOperationException(final RuntimeException exception, - final NativeWebRequest request) { + public ResponseEntity handleUnknownOperationException(final RuntimeException exception, + final NativeWebRequest request) { LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, "There was a problem processing your request."), request); } @ExceptionHandler(UnknownStorageTypeException.class) - default ResponseEntity handleUnknownStorageTypeException( + public ResponseEntity handleUnknownStorageTypeException( final UnknownStorageTypeException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -442,7 +462,7 @@ default ResponseEntity handleUnknownStorageTypeException( } @ExceptionHandler(UnprocessableEntityException.class) - default ResponseEntity handleUnprocessableEntityException( + public ResponseEntity handleUnprocessableEntityException( final UnprocessableEntityException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -450,7 +470,7 @@ default ResponseEntity handleUnprocessableEntityException( } @ExceptionHandler(UnprocessableSubscriptionException.class) - default ResponseEntity handleUnprocessableSubscriptionException( + public ResponseEntity handleUnprocessableSubscriptionException( final UnprocessableSubscriptionException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); @@ -458,16 +478,22 @@ default ResponseEntity handleUnprocessableSubscriptionException( } @ExceptionHandler(WrongInitialCursorsException.class) - default ResponseEntity handleWrongInitialCursorsException(final WrongInitialCursorsException exception, - final NativeWebRequest request) { + public ResponseEntity handleWrongInitialCursorsException(final WrongInitialCursorsException exception, + final NativeWebRequest request) { LOG.debug("Error occurred when working with subscriptions", exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(WrongStreamParametersException.class) - default ResponseEntity handleWrongStreamParametersException(final WrongStreamParametersException exception, - final NativeWebRequest request) { + public ResponseEntity handleWrongStreamParametersException(final WrongStreamParametersException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage(), exception); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidationException(final ValidationException exception, + final NativeWebRequest request) { + return create(new ValidationProblem(exception.getErrors()), request); + } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java new file mode 100644 index 0000000000..7a4f4c9354 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java @@ -0,0 +1,35 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.PostSubscriptionController; +import org.zalando.nakadi.exceptions.runtime.SubscriptionCreationDisabledException; +import org.zalando.nakadi.exceptions.runtime.SubscriptionUpdateConflictException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = PostSubscriptionController.class) +public class PostSubscriptionHandler implements AdviceTrait { + + @ExceptionHandler(SubscriptionUpdateConflictException.class) + public ResponseEntity handleSubscriptionUpdateConflictException( + final SubscriptionUpdateConflictException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(SubscriptionCreationDisabledException.class) + public ResponseEntity handleSubscriptionCreationDisabledException( + final SubscriptionCreationDisabledException exception, + final NativeWebRequest request) { + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/exceptions/runtime/BlockedException.java b/src/main/java/org/zalando/nakadi/exceptions/runtime/BlockedException.java new file mode 100644 index 0000000000..0a906b42f0 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/exceptions/runtime/BlockedException.java @@ -0,0 +1,8 @@ +package org.zalando.nakadi.exceptions.runtime; + +public class BlockedException extends NakadiBaseException { + + public BlockedException(final String message) { + super(message); + } +} diff --git a/src/main/java/org/zalando/nakadi/exceptions/runtime/SubscriptionCreationDisabledException.java b/src/main/java/org/zalando/nakadi/exceptions/runtime/SubscriptionCreationDisabledException.java new file mode 100644 index 0000000000..92ca11a43c --- /dev/null +++ b/src/main/java/org/zalando/nakadi/exceptions/runtime/SubscriptionCreationDisabledException.java @@ -0,0 +1,8 @@ +package org.zalando.nakadi.exceptions.runtime; + +public class SubscriptionCreationDisabledException extends NakadiBaseException { + + public SubscriptionCreationDisabledException(final String message) { + super(message); + } +} diff --git a/src/main/java/org/zalando/nakadi/exceptions/runtime/ValidationException.java b/src/main/java/org/zalando/nakadi/exceptions/runtime/ValidationException.java new file mode 100644 index 0000000000..8a4ec150e0 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/exceptions/runtime/ValidationException.java @@ -0,0 +1,17 @@ +package org.zalando.nakadi.exceptions.runtime; + +import org.springframework.validation.Errors; + +public class ValidationException extends NakadiBaseException { + + private final Errors errors; + + public ValidationException(final Errors errors) { + super(); + this.errors = errors; + } + + public Errors getErrors() { + return errors; + } +} diff --git a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java index 520a8ec8b0..30f1df3e42 100644 --- a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java @@ -8,6 +8,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.zalando.nakadi.config.SecuritySettings; +import org.zalando.nakadi.controller.advice.CursorsHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.domain.CursorError; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.NakadiCursor; @@ -103,6 +105,7 @@ public CursorsControllerTest() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) + .setControllerAdvice(new NakadiProblemHandling(), new CursorsHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java b/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java index 65749fce4d..5241197a39 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java @@ -12,6 +12,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.zalando.nakadi.config.SecuritySettings; +import org.zalando.nakadi.controller.advice.EventPublishingHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.domain.BatchItemResponse; import org.zalando.nakadi.domain.EventPublishResult; import org.zalando.nakadi.domain.EventPublishingStatus; @@ -92,6 +94,7 @@ public void setUp() { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) + .setControllerAdvice(new NakadiProblemHandling(), new EventPublishingHandler()) .build(); } @@ -159,7 +162,7 @@ public void whenResultIsAbortedThen207() throws Exception { @Test public void whenEventTypeNotFoundThen404() throws Exception { Mockito - .doThrow(NoSuchEventTypeException.class) + .doThrow(new NoSuchEventTypeException("topic not found")) .when(publisher) .publish(any(String.class), eq(TOPIC)); diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java index 53b274184d..d51f8d30f5 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java @@ -11,6 +11,7 @@ import org.zalando.nakadi.config.NakadiSettings; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.config.ValidatorConfig; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.EventTypeBase; import org.zalando.nakadi.domain.Timeline; @@ -118,7 +119,7 @@ public void init() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemControllerAdvice()) + .setControllerAdvice(new NakadiProblemHandling()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java b/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java index acb108364a..c3daadd0cd 100644 --- a/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java +++ b/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java @@ -7,6 +7,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; import org.zalando.problem.Problem; @@ -16,7 +17,7 @@ public class NakadiProblemHandlingTest { @Test public void testIllegalClientIdException() { - final NakadiProblemHandling nakadiProblemHandling = new NakadiProblemControllerAdvice(); + final NakadiProblemHandling nakadiProblemHandling = new NakadiProblemHandling(); final NativeWebRequest mockedRequest = Mockito.mock(NativeWebRequest.class); Mockito.when(mockedRequest.getHeader(Matchers.any())).thenReturn(""); diff --git a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java index 08e39befde..9d0cd8d565 100644 --- a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java @@ -8,6 +8,7 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.test.web.servlet.MockMvc; import org.zalando.nakadi.config.SecuritySettings; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.NakadiCursor; import org.zalando.nakadi.domain.NakadiCursorLag; @@ -94,7 +95,7 @@ public void before() throws InternalNakadiException, NoSuchEventTypeException { timelineService = Mockito.mock(TimelineService.class); cursorOperationsService = Mockito.mock(CursorOperationsService.class); when(timelineService.getActiveTimelinesOrdered(eq(UNKNOWN_EVENT_TYPE))) - .thenThrow(NoSuchEventTypeException.class); + .thenThrow(new NoSuchEventTypeException("topic not found")); when(timelineService.getActiveTimelinesOrdered(eq(TEST_EVENT_TYPE))) .thenReturn(Collections.singletonList(TIMELINE)); when(timelineService.getAllTimelinesOrdered(eq(TEST_EVENT_TYPE))) @@ -111,7 +112,7 @@ public void before() throws InternalNakadiException, NoSuchEventTypeException { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemControllerAdvice()) + .setControllerAdvice(new NakadiProblemHandling()) .build(); } @@ -233,7 +234,8 @@ private List mockCursorLag() { @Test public void whenGetPartitionForWrongTopicThenNotFound() throws Exception { - when(eventTypeRepositoryMock.findByName(UNKNOWN_EVENT_TYPE)).thenThrow(NoSuchEventTypeException.class); + when(eventTypeRepositoryMock.findByName(UNKNOWN_EVENT_TYPE)) + .thenThrow(new NoSuchEventTypeException("topic not found")); final ThrowableProblem expectedProblem = Problem.valueOf(NOT_FOUND, "topic not found"); mockMvc.perform( diff --git a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java index 9cbda1590b..1b5ab23a9e 100644 --- a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java @@ -7,13 +7,15 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.PostSubscriptionHandler; import org.zalando.nakadi.domain.Subscription; import org.zalando.nakadi.domain.SubscriptionBase; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; @@ -45,7 +47,7 @@ public class PostSubscriptionControllerTest { private static final String PROBLEM_CONTENT_TYPE = "application/problem+json"; - private final StandaloneMockMvcBuilder mockMvcBuilder; + private final MockMvc mockMvc; private final ApplicationService applicationService = mock(ApplicationService.class); private final FeatureToggleService featureToggleService = mock(FeatureToggleService.class); @@ -65,10 +67,11 @@ public PostSubscriptionControllerTest() throws Exception { final PostSubscriptionController controller = new PostSubscriptionController(featureToggleService, subscriptionService); - mockMvcBuilder = standaloneSetup(controller) + mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new NakadiProblemControllerAdvice()) - .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()); + .setControllerAdvice(new PostSubscriptionHandler(), new NakadiProblemHandling()) + .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()) + .build(); } @Test @@ -217,7 +220,7 @@ private ResultActions postSubscriptionAsJson(final String subscription) throws E final MockHttpServletRequestBuilder requestBuilder = post("/subscriptions") .contentType(APPLICATION_JSON) .content(subscription); - return mockMvcBuilder.build().perform(requestBuilder); + return mockMvc.perform(requestBuilder); } private class TestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { diff --git a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java index 2381b42983..67ff5b93ae 100644 --- a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java @@ -4,13 +4,15 @@ import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import org.zalando.nakadi.config.NakadiSettings; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.PostSubscriptionHandler; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.EventTypeBase; import org.zalando.nakadi.domain.EventTypePartition; @@ -92,7 +94,7 @@ public class SubscriptionControllerTest { private final SubscriptionDbRepository subscriptionRepository = mock(SubscriptionDbRepository.class); private final EventTypeRepository eventTypeRepository = mock(EventTypeRepository.class); - private final StandaloneMockMvcBuilder mockMvcBuilder; + private final MockMvc mockMvc; private final TopicRepository topicRepository; private final ZkSubscriptionClient zkSubscriptionClient; private final CursorConverter cursorConverter; @@ -128,10 +130,11 @@ public SubscriptionControllerTest() throws Exception { final ApplicationService applicationService = mock(ApplicationService.class); doReturn(true).when(applicationService).exists(any()); - mockMvcBuilder = standaloneSetup(controller) + mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new NakadiProblemControllerAdvice()) - .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()); + .setControllerAdvice(new NakadiProblemHandling(), new PostSubscriptionHandler()) + .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()) + .build(); } @Test @@ -285,23 +288,23 @@ public void whenGetSubscriptionNoEventTypesThenStatEmpty() throws Exception { } private ResultActions getSubscriptionStats(final String subscriptionId) throws Exception { - return mockMvcBuilder.build().perform(get(format("/subscriptions/{0}/stats", subscriptionId))); + return mockMvc.perform(get(format("/subscriptions/{0}/stats", subscriptionId))); } private ResultActions getSubscriptions() throws Exception { - return mockMvcBuilder.build().perform(get("/subscriptions")); + return mockMvc.perform(get("/subscriptions")); } private ResultActions getSubscriptions(final Set eventTypes, final String owningApp, final int offset, final int limit) throws Exception { final String url = createSubscriptionListUri(Optional.of(owningApp), eventTypes, offset, limit, false); - return mockMvcBuilder.build().perform(get(url)); + return mockMvc.perform(get(url)); } @Test public void whenDeleteSubscriptionThenNoContent() throws Exception { mockGetFromRepoSubscriptionWithOwningApp("sid", "nakadiClientId"); - mockMvcBuilder.build().perform(delete("/subscriptions/sid")) + mockMvc.perform(delete("/subscriptions/sid")) .andExpect(status().isNoContent()); } @@ -313,7 +316,7 @@ public void whenDeleteSubscriptionThatDoesNotExistThenNotFound() throws Exceptio .when(subscriptionRepository).deleteSubscription("sid"); checkForProblem( - mockMvcBuilder.build().perform(delete("/subscriptions/sid")), + mockMvc.perform(delete("/subscriptions/sid")), Problem.valueOf(NOT_FOUND, "dummy message")); } @@ -327,7 +330,7 @@ private void mockGetFromRepoSubscriptionWithOwningApp(final String subscriptionI } private ResultActions getSubscription(final String subscriptionId) throws Exception { - return mockMvcBuilder.build().perform(get(format("/subscriptions/{0}", subscriptionId))); + return mockMvc.perform(get(format("/subscriptions/{0}", subscriptionId))); } private void checkForProblem(final ResultActions resultActions, final Problem expectedProblem) throws Exception { diff --git a/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java b/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java index 22ab92a9ed..7b646c506d 100644 --- a/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java @@ -11,6 +11,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.zalando.nakadi.config.SecuritySettings; +import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.domain.Storage; import org.zalando.nakadi.domain.Timeline; import org.zalando.nakadi.security.ClientResolver; @@ -42,7 +43,7 @@ public TimelinesControllerTest() { mockMvc = MockMvcBuilders.standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(securitySettings, featureToggleService)) - .setControllerAdvice(new NakadiProblemControllerAdvice()) + .setControllerAdvice(new NakadiProblemHandling()) .build(); } From 6091c2c226919c9d25ad40ee2fb4d7aef9f227a3 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Mon, 8 Oct 2018 17:07:39 +0200 Subject: [PATCH 08/23] Fix tests --- .../org/zalando/nakadi/webservice/PartitionsControllerAT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/acceptance-test/java/org/zalando/nakadi/webservice/PartitionsControllerAT.java b/src/acceptance-test/java/org/zalando/nakadi/webservice/PartitionsControllerAT.java index 44522e7bc1..66e48de0c8 100644 --- a/src/acceptance-test/java/org/zalando/nakadi/webservice/PartitionsControllerAT.java +++ b/src/acceptance-test/java/org/zalando/nakadi/webservice/PartitionsControllerAT.java @@ -98,7 +98,7 @@ public void whenListPartitionsThenTopicNotFound() throws IOException { .then() .statusCode(HttpStatus.NOT_FOUND.value()) .and() - .body("detail", equalTo("topic not found")); + .body("detail", equalTo("EventType \"not-existing-topic\" does not exist.")); } @Test @@ -145,7 +145,7 @@ public void whenGetPartitionThenTopicNotFound() throws IOException { .then() .statusCode(HttpStatus.NOT_FOUND.value()) .and() - .body("detail", equalTo("topic not found")); + .body("detail", equalTo("EventType \"not-existing-topic\" does not exist.")); } @Test From cdeeed16512a915e9cefed9fe5045d8620db2a4b Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Wed, 10 Oct 2018 16:38:32 +0200 Subject: [PATCH 09/23] Fixes post review --- .../nakadi/config/SecurityConfiguration.java | 1 - .../controller/EventPublishingController.java | 10 ----- .../controller/EventTypeController.java | 9 ++-- .../nakadi/controller/SettingsController.java | 43 +++++++++---------- .../advice/NakadiProblemHandling.java | 8 ++++ .../runtime/ForbiddenOperationException.java | 8 ++++ 6 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/zalando/nakadi/exceptions/runtime/ForbiddenOperationException.java diff --git a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java index e906f79c5f..3ff91579d2 100644 --- a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java +++ b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java @@ -200,5 +200,4 @@ private static Status fromStatusCode(final int code) { } return null; } - } diff --git a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java index 720b57f321..18563c78e7 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java @@ -1,7 +1,6 @@ package org.zalando.nakadi.controller; import com.google.common.base.Charsets; -import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -108,15 +107,6 @@ private ResponseEntity postEventInternal(final String eventTypeName, reportSLOs(startingNanos, totalSizeBytes, eventCount, result, eventTypeName, client); return response(result); - } catch (final JSONException e) { - LOG.debug("Problem parsing event", e); - throw e; - } catch (final NoSuchEventTypeException e) { - LOG.debug("Event type not found.", e.getMessage()); - throw e; - } catch (final EventTypeTimeoutException e) { - LOG.debug("Failed to publish batch", e); - throw e; } finally { eventTypeMetrics.updateTiming(startingNanos, System.nanoTime()); } diff --git a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java index 2af0bd8bda..8d31c61811 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java @@ -20,6 +20,8 @@ import org.zalando.nakadi.exceptions.runtime.ConflictException; import org.zalando.nakadi.exceptions.runtime.DuplicatedEventTypeNameException; import org.zalando.nakadi.exceptions.runtime.EventTypeDeletionException; +import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; +import org.zalando.nakadi.exceptions.runtime.ForbiddenOperationException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidEventTypeException; @@ -76,9 +78,10 @@ public ResponseEntity create(@Valid @RequestBody final EventTypeBase eventTyp final Errors errors, final NativeWebRequest request) throws TopicCreationException, InternalNakadiException, NoSuchPartitionStrategyException, - DuplicatedEventTypeNameException, InvalidEventTypeException, ValidationException { + DuplicatedEventTypeNameException, InvalidEventTypeException, ValidationException, + FeatureNotAvailableException { if (featureToggleService.isFeatureEnabled(DISABLE_EVENT_TYPE_CREATION)) { - return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + throw new FeatureNotAvailableException("Event Type creation is disabled", DISABLE_EVENT_TYPE_CREATION); } if (errors.hasErrors()) { @@ -99,7 +102,7 @@ public ResponseEntity delete(@PathVariable("name") final String eventTypeName ServiceTemporarilyUnavailableException { if (featureToggleService.isFeatureEnabled(DISABLE_EVENT_TYPE_DELETION) && !adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return new ResponseEntity<>(HttpStatus.FORBIDDEN); + throw new ForbiddenOperationException("Event Type deletion is disabled"); } eventTypeService.delete(eventTypeName); diff --git a/src/main/java/org/zalando/nakadi/controller/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index f1520fabf9..a7b005177a 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -1,7 +1,6 @@ package org.zalando.nakadi.controller; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.PathVariable; @@ -9,13 +8,12 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.ResourceAuthorization; +import org.zalando.nakadi.exceptions.runtime.ForbiddenOperationException; import org.zalando.nakadi.exceptions.runtime.ValidationException; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; -import org.zalando.nakadi.security.Client; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.BlacklistService; import org.zalando.nakadi.service.FeatureToggleService; @@ -46,18 +44,19 @@ public SettingsController(final BlacklistService blacklistService, } @RequestMapping(path = "/blacklist", method = RequestMethod.GET) - public ResponseEntity getBlacklist() { + public ResponseEntity getBlacklist() throws ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.READ)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } return ResponseEntity.ok(blacklistService.getBlacklist()); } @RequestMapping(value = "/blacklist/{blacklist_type}/{name}", method = RequestMethod.PUT) public ResponseEntity blacklist(@PathVariable("blacklist_type") final BlacklistService.Type blacklistType, - @PathVariable("name") final String name) { + @PathVariable("name") final String name) + throws ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } blacklistService.blacklist(name, blacklistType); return ResponseEntity.noContent().build(); @@ -65,46 +64,48 @@ public ResponseEntity blacklist(@PathVariable("blacklist_type") final BlacklistS @RequestMapping(value = "/blacklist/{blacklist_type}/{name}", method = RequestMethod.DELETE) public ResponseEntity whitelist(@PathVariable("blacklist_type") final BlacklistService.Type blacklistType, - @PathVariable("name") final String name) { + @PathVariable("name") final String name) + throws ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } blacklistService.whitelist(name, blacklistType); return ResponseEntity.noContent().build(); } @RequestMapping(path = "/features", method = RequestMethod.GET) - public ResponseEntity getFeatures(final Client client) { + public ResponseEntity getFeatures() + throws ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.READ)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } return ResponseEntity.ok(new ItemsWrapper<>(featureToggleService.getFeatures())); } @RequestMapping(path = "/features", method = RequestMethod.POST) - public ResponseEntity setFeature(@RequestBody final FeatureToggleService.FeatureWrapper featureWrapper, - final Client client) { + public ResponseEntity setFeature(@RequestBody final FeatureToggleService.FeatureWrapper featureWrapper) + throws ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } featureToggleService.setFeature(featureWrapper); return ResponseEntity.noContent().build(); } @RequestMapping(path = "/admins", method = RequestMethod.GET) - public ResponseEntity getAdmins() { + public ResponseEntity getAdmins() throws ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.READ)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } return ResponseEntity.ok(ResourceAuthorization.fromPermissionsList(adminService.getAdmins())); } @RequestMapping(path = "/admins", method = RequestMethod.POST) public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthorization authz, - final Errors errors, - final NativeWebRequest request) throws ValidationException { + final Errors errors) + throws ValidationException, ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.ADMIN)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges are required to perform this operation"); } if (errors.hasErrors()) { throw new ValidationException(errors); @@ -112,8 +113,4 @@ public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthoriz adminService.updateAdmins(authz.toPermissionsList(ADMIN_RESOURCE)); return ResponseEntity.ok().build(); } - - private boolean isNotAdmin(final Client client) { - return !client.getClientId().equals(securitySettings.getAdminClientId()); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java index 4a04139b9c..a58d11cd51 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java @@ -25,6 +25,7 @@ import org.zalando.nakadi.exceptions.runtime.EventTypeOptionsValidationException; import org.zalando.nakadi.exceptions.runtime.EventTypeUnavailableException; import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; +import org.zalando.nakadi.exceptions.runtime.ForbiddenOperationException; import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; @@ -496,4 +497,11 @@ public ResponseEntity handleValidationException(final ValidationExcepti final NativeWebRequest request) { return create(new ValidationProblem(exception.getErrors()), request); } + + @ExceptionHandler(ForbiddenOperationException.class) + public ResponseEntity handleForbiddenOperationException(final ForbiddenOperationException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage()); + return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); + } } diff --git a/src/main/java/org/zalando/nakadi/exceptions/runtime/ForbiddenOperationException.java b/src/main/java/org/zalando/nakadi/exceptions/runtime/ForbiddenOperationException.java new file mode 100644 index 0000000000..a8f2d7218c --- /dev/null +++ b/src/main/java/org/zalando/nakadi/exceptions/runtime/ForbiddenOperationException.java @@ -0,0 +1,8 @@ +package org.zalando.nakadi.exceptions.runtime; + +public class ForbiddenOperationException extends NakadiBaseException { + + public ForbiddenOperationException(final String message) { + super(message); + } +} From 935e4ad4c85290766fc982edbc464b7a6e41c67a Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 11:12:25 +0200 Subject: [PATCH 10/23] Remove unused field --- .../org/zalando/nakadi/controller/SettingsController.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/controller/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index a7b005177a..3f41fda00c 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.ResourceAuthorization; import org.zalando.nakadi.exceptions.runtime.ForbiddenOperationException; @@ -28,18 +27,15 @@ public class SettingsController { private final BlacklistService blacklistService; private final FeatureToggleService featureToggleService; - private final SecuritySettings securitySettings; private final AdminService adminService; @Autowired public SettingsController(final BlacklistService blacklistService, final FeatureToggleService featureToggleService, - final SecuritySettings securitySettings, final AdminService adminService) { this.blacklistService = blacklistService; this.featureToggleService = featureToggleService; - this.securitySettings = securitySettings; this.adminService = adminService; } From 2ea0ba3c48d98cbbff4d15a1d423ced28ef31318 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 16:48:09 +0200 Subject: [PATCH 11/23] Move exception handlers to controller-specific ControllerAdvices when appropriate --- .../CursorOperationsController.java | 6 - .../nakadi/controller/SchemaController.java | 12 - .../nakadi/controller/StoragesController.java | 6 + .../controller/SubscriptionController.java | 3 + .../advice/CursorOperationsHandler.java | 8 + .../controller/advice/CursorsHandler.java | 27 ++ .../advice/EventPublishingHandler.java | 14 + .../controller/advice/EventTypeHandler.java | 69 +++++ .../advice/NakadiProblemHandling.java | 281 ------------------ .../controller/advice/PartitionsHandler.java | 36 +++ .../advice/PostSubscriptionHandler.java | 20 ++ .../controller/advice/SchemaHandler.java | 27 ++ .../controller/advice/SettingsHandler.java | 35 +++ .../controller/advice/StoragesHandler.java | 57 ++++ .../advice/SubscriptionHandler.java | 45 +++ .../advice/SubscriptionStreamHandler.java | 35 +++ .../controller/advice/TimelinesHandler.java | 60 ++++ .../EventTypeControllerTestCase.java | 3 +- .../controller/PartitionsControllerTest.java | 3 +- 19 files changed, 446 insertions(+), 301 deletions(-) create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java create mode 100644 src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java diff --git a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java index 3d9565d482..d72fe6c77a 100644 --- a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java +++ b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java @@ -4,13 +4,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.NakadiCursor; import org.zalando.nakadi.domain.NakadiCursorLag; @@ -18,7 +16,6 @@ import org.zalando.nakadi.exceptions.runtime.CursorConversionException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidCursorException; -import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NotFoundException; @@ -32,9 +29,6 @@ import org.zalando.nakadi.view.CursorDistance; import org.zalando.nakadi.view.CursorLag; import org.zalando.nakadi.view.ShiftedCursor; -import org.zalando.problem.MoreStatus; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.validation.Valid; import java.util.List; diff --git a/src/main/java/org/zalando/nakadi/controller/SchemaController.java b/src/main/java/org/zalando/nakadi/controller/SchemaController.java index 0272f1e480..37d7c13905 100644 --- a/src/main/java/org/zalando/nakadi/controller/SchemaController.java +++ b/src/main/java/org/zalando/nakadi/controller/SchemaController.java @@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -21,10 +20,6 @@ import org.zalando.nakadi.exceptions.runtime.NoSuchSchemaException; import org.zalando.nakadi.service.EventTypeService; import org.zalando.nakadi.service.SchemaService; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; - -import static javax.ws.rs.core.Response.Status.NOT_FOUND; @RestController public class SchemaController { @@ -68,11 +63,4 @@ public ResponseEntity getSchemaVersion(@PathVariable("name") final String nam final EventTypeSchema result = schemaService.getSchemaVersion(name, version); return ResponseEntity.status(HttpStatus.OK).body(result); } - - @ExceptionHandler(NoSuchSchemaException.class) - public ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(NOT_FOUND, exception.getMessage(), request); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/StoragesController.java b/src/main/java/org/zalando/nakadi/controller/StoragesController.java index 058c0ac985..5e320080a7 100644 --- a/src/main/java/org/zalando/nakadi/controller/StoragesController.java +++ b/src/main/java/org/zalando/nakadi/controller/StoragesController.java @@ -10,7 +10,13 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.domain.Storage; +import org.zalando.nakadi.exceptions.runtime.DbWriteOperationsBlockedException; +import org.zalando.nakadi.exceptions.runtime.DuplicatedStorageException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; +import org.zalando.nakadi.exceptions.runtime.NoSuchStorageException; +import org.zalando.nakadi.exceptions.runtime.StorageIsUsedException; +import org.zalando.nakadi.exceptions.runtime.UnknownStorageTypeException; +import org.zalando.nakadi.plugin.api.PluginException; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.StorageService; diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java index 8137f7508d..73242840a2 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java @@ -10,7 +10,10 @@ import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.SubscriptionEventTypeStats; +import org.zalando.nakadi.exceptions.runtime.DbWriteOperationsBlockedException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; +import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; +import org.zalando.nakadi.exceptions.runtime.InvalidLimitException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java index e4d303d006..e0b469db1b 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java @@ -5,6 +5,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.controller.CursorOperationsController; +import org.zalando.nakadi.exceptions.runtime.CursorConversionException; import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.problem.Problem; @@ -27,6 +28,13 @@ public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperati clientErrorMessage(exception.getReason())), request); } + @ExceptionHandler(CursorConversionException.class) + public ResponseEntity handleCursorConversionException(final CursorConversionException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + private String clientErrorMessage(final InvalidCursorOperation.Reason reason) { switch (reason) { case TIMELINE_NOT_FOUND: return "Timeline not found. It might happen in case the cursor refers to a " + diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java index 6bbe287cdc..63d463c9e4 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java @@ -6,8 +6,11 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.controller.CursorsController; +import org.zalando.nakadi.exceptions.runtime.CursorsAreEmptyException; import org.zalando.nakadi.exceptions.runtime.InvalidCursorException; +import org.zalando.nakadi.exceptions.runtime.InvalidStreamIdException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; +import org.zalando.nakadi.exceptions.runtime.RequestInProgressException; import org.zalando.nakadi.exceptions.runtime.UnableProcessException; import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.problem.Problem; @@ -15,6 +18,7 @@ import javax.annotation.Priority; +import static org.zalando.problem.Status.CONFLICT; import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @@ -39,12 +43,35 @@ public ResponseEntity handleMethodArgumentNotValid(final MethodArgument @ExceptionHandler(NoSuchEventTypeException.class) public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, final NativeWebRequest request) { + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(InvalidCursorException.class) public ResponseEntity handleInvalidCursorException(final InvalidCursorException exception, final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(InvalidStreamIdException.class) + public ResponseEntity handleInvalidStreamIdException(final InvalidStreamIdException exception, + final NativeWebRequest request) { + LOG.warn("Stream id {} is not found: {}", exception.getStreamId(), exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(RequestInProgressException.class) + public ResponseEntity handleRequestInProgressException(final RequestInProgressException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(CursorsAreEmptyException.class) + public ResponseEntity handleCursorsAreEmptyException(final RuntimeException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java index 0b4241e211..78469dc217 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java @@ -6,7 +6,11 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.controller.EventPublishingController; +import org.zalando.nakadi.exceptions.runtime.EnrichmentException; import org.zalando.nakadi.exceptions.runtime.EventTypeTimeoutException; +import org.zalando.nakadi.exceptions.runtime.InvalidPartitionKeyFieldsException; +import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; +import org.zalando.nakadi.exceptions.runtime.PartitioningException; import org.zalando.problem.Problem; import org.zalando.problem.spring.web.advice.AdviceTrait; @@ -14,6 +18,7 @@ import static org.zalando.problem.Status.BAD_REQUEST; import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @Priority(10) @ControllerAdvice(assignableTypes = EventPublishingController.class) @@ -34,5 +39,14 @@ public ResponseEntity handleJSONException(final JSONException exception } return create(Problem.valueOf(BAD_REQUEST), request); } + + @ExceptionHandler({EnrichmentException.class, + PartitioningException.class, + InvalidPartitionKeyFieldsException.class}) + public ResponseEntity handleUnprocessableEntityResponses(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java new file mode 100644 index 0000000000..a66a1b7930 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java @@ -0,0 +1,69 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.EventTypeController; +import org.zalando.nakadi.exceptions.runtime.ConflictException; +import org.zalando.nakadi.exceptions.runtime.DuplicatedEventTypeNameException; +import org.zalando.nakadi.exceptions.runtime.EventTypeDeletionException; +import org.zalando.nakadi.exceptions.runtime.EventTypeOptionsValidationException; +import org.zalando.nakadi.exceptions.runtime.EventTypeUnavailableException; +import org.zalando.nakadi.exceptions.runtime.InvalidEventTypeException; +import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; +import org.zalando.nakadi.exceptions.runtime.NoSuchPartitionStrategyException; +import org.zalando.nakadi.exceptions.runtime.TopicCreationException; +import org.zalando.nakadi.exceptions.runtime.UnableProcessException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = EventTypeController.class) +public class EventTypeHandler implements AdviceTrait { + + @ExceptionHandler(NoSuchPartitionStrategyException.class) + public ResponseEntity handleNoSuchPartitionStrategyException( + final NoSuchPartitionStrategyException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler({InvalidEventTypeException.class, + UnableProcessException.class, + EventTypeOptionsValidationException.class}) + public ResponseEntity handleUnprocessableEntityResponses(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(EventTypeDeletionException.class) + public ResponseEntity handleEventTypeDeletionException(final EventTypeDeletionException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + + @ExceptionHandler({DuplicatedEventTypeNameException.class, ConflictException.class}) + public ResponseEntity handleConflictResponses(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler({EventTypeUnavailableException.class, TopicCreationException.class}) + public ResponseEntity handleServiceUnavailableResponses(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java index a58d11cd51..aec89f4d9a 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java @@ -12,56 +12,23 @@ import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.exceptions.runtime.AccessDeniedException; import org.zalando.nakadi.exceptions.runtime.BlockedException; -import org.zalando.nakadi.exceptions.runtime.CompactionException; -import org.zalando.nakadi.exceptions.runtime.ConflictException; -import org.zalando.nakadi.exceptions.runtime.CursorConversionException; -import org.zalando.nakadi.exceptions.runtime.CursorsAreEmptyException; import org.zalando.nakadi.exceptions.runtime.DbWriteOperationsBlockedException; -import org.zalando.nakadi.exceptions.runtime.DuplicatedEventTypeNameException; -import org.zalando.nakadi.exceptions.runtime.DuplicatedStorageException; -import org.zalando.nakadi.exceptions.runtime.EnrichmentException; -import org.zalando.nakadi.exceptions.runtime.ErrorGettingCursorTimeLagException; -import org.zalando.nakadi.exceptions.runtime.EventTypeDeletionException; -import org.zalando.nakadi.exceptions.runtime.EventTypeOptionsValidationException; -import org.zalando.nakadi.exceptions.runtime.EventTypeUnavailableException; import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; import org.zalando.nakadi.exceptions.runtime.ForbiddenOperationException; import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; -import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; -import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; -import org.zalando.nakadi.exceptions.runtime.InvalidEventTypeException; import org.zalando.nakadi.exceptions.runtime.InvalidLimitException; -import org.zalando.nakadi.exceptions.runtime.InvalidPartitionKeyFieldsException; -import org.zalando.nakadi.exceptions.runtime.InvalidStreamIdException; import org.zalando.nakadi.exceptions.runtime.InvalidVersionNumberException; import org.zalando.nakadi.exceptions.runtime.LimitReachedException; import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; -import org.zalando.nakadi.exceptions.runtime.NoStreamingSlotsAvailable; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; -import org.zalando.nakadi.exceptions.runtime.NoSuchPartitionStrategyException; import org.zalando.nakadi.exceptions.runtime.NoSuchSchemaException; -import org.zalando.nakadi.exceptions.runtime.NoSuchStorageException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; -import org.zalando.nakadi.exceptions.runtime.NotFoundException; -import org.zalando.nakadi.exceptions.runtime.PartitioningException; import org.zalando.nakadi.exceptions.runtime.RepositoryProblemException; -import org.zalando.nakadi.exceptions.runtime.RequestInProgressException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; -import org.zalando.nakadi.exceptions.runtime.StorageIsUsedException; -import org.zalando.nakadi.exceptions.runtime.TimeLagStatsTimeoutException; -import org.zalando.nakadi.exceptions.runtime.TimelineException; -import org.zalando.nakadi.exceptions.runtime.TooManyPartitionsException; -import org.zalando.nakadi.exceptions.runtime.TopicCreationException; -import org.zalando.nakadi.exceptions.runtime.UnableProcessException; -import org.zalando.nakadi.exceptions.runtime.UnknownOperationException; -import org.zalando.nakadi.exceptions.runtime.UnknownStorageTypeException; import org.zalando.nakadi.exceptions.runtime.UnprocessableEntityException; -import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; import org.zalando.nakadi.exceptions.runtime.ValidationException; -import org.zalando.nakadi.exceptions.runtime.WrongInitialCursorsException; -import org.zalando.nakadi.exceptions.runtime.WrongStreamParametersException; import org.zalando.nakadi.problem.ValidationProblem; import org.zalando.problem.Problem; import org.zalando.problem.spring.web.advice.ProblemHandling; @@ -69,12 +36,10 @@ import javax.annotation.Priority; import static org.zalando.problem.Status.BAD_REQUEST; -import static org.zalando.problem.Status.CONFLICT; import static org.zalando.problem.Status.FORBIDDEN; import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; import static org.zalando.problem.Status.NOT_FOUND; import static org.zalando.problem.Status.NOT_IMPLEMENTED; -import static org.zalando.problem.Status.REQUEST_TIMEOUT; import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; import static org.zalando.problem.Status.TOO_MANY_REQUESTS; import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @@ -84,7 +49,6 @@ public class NakadiProblemHandling implements ProblemHandling { private static final Logger LOG = LoggerFactory.getLogger(NakadiProblemHandling.class); - static final String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; @Override public String formatFieldName(final String fieldName) { @@ -135,34 +99,6 @@ public ResponseEntity handleBlockedException(final BlockedException exc return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); } - @ExceptionHandler(CompactionException.class) - public ResponseEntity handleCompactionException(final CompactionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(ConflictException.class) - public ResponseEntity handleConflictException(final ConflictException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); - } - - @ExceptionHandler(CursorsAreEmptyException.class) - public ResponseEntity handleCursorsAreEmptyException(final RuntimeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(CursorConversionException.class) - public ResponseEntity handleCursorConversionException(final CursorConversionException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(DbWriteOperationsBlockedException.class) public ResponseEntity handleDbWriteOperationsBlockedException( final DbWriteOperationsBlockedException exception, final NativeWebRequest request) { @@ -171,59 +107,6 @@ public ResponseEntity handleDbWriteOperationsBlockedException( "Database is currently in read-only mode"), request); } - @ExceptionHandler(DuplicatedEventTypeNameException.class) - public ResponseEntity handleDuplicatedEventTypeNameException( - final DuplicatedEventTypeNameException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); - } - - @ExceptionHandler(DuplicatedStorageException.class) - public ResponseEntity handleDuplicatedStorageException( - final DuplicatedStorageException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); - } - - @ExceptionHandler(EnrichmentException.class) - public ResponseEntity handleEnrichmentException(final EnrichmentException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(ErrorGettingCursorTimeLagException.class) - public ResponseEntity handleErrorGettingCursorTimeLagException( - final ErrorGettingCursorTimeLagException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(EventTypeDeletionException.class) - public ResponseEntity handleEventTypeDeletionException(final EventTypeDeletionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); - } - - @ExceptionHandler(EventTypeOptionsValidationException.class) - public ResponseEntity handleEventTypeOptionsValidationException( - final EventTypeOptionsValidationException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(EventTypeUnavailableException.class) - public ResponseEntity handleEventTypeUnavailableException(final EventTypeUnavailableException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); - } - @ExceptionHandler(FeatureNotAvailableException.class) public ResponseEntity handleFeatureNotAvailableException( final FeatureNotAvailableException ex, @@ -238,13 +121,6 @@ public ResponseEntity handleIllegalClientIdException(final IllegalClien return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); } - @ExceptionHandler(InconsistentStateException.class) - public ResponseEntity handleInconsistentStateExcetpion(final InconsistentStateException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); - } - @ExceptionHandler(InternalNakadiException.class) public ResponseEntity handleInternalNakadiException(final InternalNakadiException exception, final NativeWebRequest request) { @@ -252,20 +128,6 @@ public ResponseEntity handleInternalNakadiException(final InternalNakad return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } - @ExceptionHandler(InvalidCursorOperation.class) - public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, - final NativeWebRequest request) { - LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); - } - - @ExceptionHandler(InvalidEventTypeException.class) - public ResponseEntity handleInvalidEventTypeException(final InvalidEventTypeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(InvalidLimitException.class) public ResponseEntity handleInvalidLimitException( final InvalidLimitException exception, @@ -274,21 +136,6 @@ public ResponseEntity handleInvalidLimitException( return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); } - @ExceptionHandler(InvalidPartitionKeyFieldsException.class) - public ResponseEntity handleInvalidPartitionKeyFieldsException( - final InvalidPartitionKeyFieldsException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(InvalidStreamIdException.class) - public ResponseEntity handleInvalidStreamIdException(final InvalidStreamIdException exception, - final NativeWebRequest request) { - LOG.warn("Stream id {} is not found: {}", exception.getStreamId(), exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(InvalidVersionNumberException.class) public ResponseEntity handleInvalidVersionNumberException( final InvalidVersionNumberException exception, @@ -321,20 +168,6 @@ public ResponseEntity handleNakadiRuntimeException(final NakadiRuntimeE throw exception.getException(); } - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(final NotFoundException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); - } - - @ExceptionHandler(NoStreamingSlotsAvailable.class) - public ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); - } - @ExceptionHandler(NoSuchEventTypeException.class) public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, final NativeWebRequest request) { @@ -342,14 +175,6 @@ public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventT return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } - @ExceptionHandler(NoSuchPartitionStrategyException.class) - public ResponseEntity handleNoSuchPartitionStrategyException( - final NoSuchPartitionStrategyException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(NoSuchSchemaException.class) public ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, final NativeWebRequest request) { @@ -357,14 +182,6 @@ public ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaExc return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } - @ExceptionHandler(NoSuchStorageException.class) - public ResponseEntity handleNoSuchStorageException( - final NoSuchStorageException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); - } - @ExceptionHandler(NoSuchSubscriptionException.class) public ResponseEntity handleNoSuchSubscriptionException(final NoSuchSubscriptionException exception, final NativeWebRequest request) { @@ -372,13 +189,6 @@ public ResponseEntity handleNoSuchSubscriptionException(final NoSuchSub return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } - @ExceptionHandler(PartitioningException.class) - public ResponseEntity handlePartitioningException(final PartitioningException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(RepositoryProblemException.class) public ResponseEntity handleRepositoryProblemException(final RepositoryProblemException exception, final NativeWebRequest request) { @@ -386,13 +196,6 @@ public ResponseEntity handleRepositoryProblemException(final Repository return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } - @ExceptionHandler(RequestInProgressException.class) - public ResponseEntity handleRequestInProgressException(final RequestInProgressException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); - } - @ExceptionHandler(ServiceTemporarilyUnavailableException.class) public ResponseEntity handleServiceTemporarilyUnavailableException( final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { @@ -400,68 +203,6 @@ public ResponseEntity handleServiceTemporarilyUnavailableException( return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } - @ExceptionHandler(StorageIsUsedException.class) - public ResponseEntity handleStorageIsUsedException( - final StorageIsUsedException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); - } - - @ExceptionHandler(TimeLagStatsTimeoutException.class) - public ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException exception, - final NativeWebRequest request) { - LOG.warn(exception.getMessage()); - return create(Problem.valueOf(REQUEST_TIMEOUT, exception.getMessage()), request); - } - - @ExceptionHandler(TimelineException.class) - public ResponseEntity handleTimelineException(final TimelineException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - final Throwable cause = exception.getCause(); - if (cause instanceof InternalNakadiException) { - return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); - } - return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); - } - - @ExceptionHandler(TooManyPartitionsException.class) - public ResponseEntity handleTooManyPartitionsException(final TooManyPartitionsException exception, - final NativeWebRequest request) { - LOG.debug("Error occurred when working with subscriptions", exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(TopicCreationException.class) - public ResponseEntity handleTopicCreationException(final TopicCreationException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); - } - - @ExceptionHandler(UnableProcessException.class) - public ResponseEntity handleUnableProcessException(final UnableProcessException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(UnknownOperationException.class) - public ResponseEntity handleUnknownOperationException(final RuntimeException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return create(Problem.valueOf(SERVICE_UNAVAILABLE, "There was a problem processing your request."), request); - } - - @ExceptionHandler(UnknownStorageTypeException.class) - public ResponseEntity handleUnknownStorageTypeException( - final UnknownStorageTypeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(UnprocessableEntityException.class) public ResponseEntity handleUnprocessableEntityException( final UnprocessableEntityException exception, @@ -470,28 +211,6 @@ public ResponseEntity handleUnprocessableEntityException( return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } - @ExceptionHandler(UnprocessableSubscriptionException.class) - public ResponseEntity handleUnprocessableSubscriptionException( - final UnprocessableSubscriptionException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(WrongInitialCursorsException.class) - public ResponseEntity handleWrongInitialCursorsException(final WrongInitialCursorsException exception, - final NativeWebRequest request) { - LOG.debug("Error occurred when working with subscriptions", exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(WrongStreamParametersException.class) - public ResponseEntity handleWrongStreamParametersException(final WrongStreamParametersException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler(ValidationException.class) public ResponseEntity handleValidationException(final ValidationException exception, final NativeWebRequest request) { diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java new file mode 100644 index 0000000000..cdc23d977a --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java @@ -0,0 +1,36 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.PartitionsController; +import org.zalando.nakadi.exceptions.runtime.InvalidCursorOperation; +import org.zalando.nakadi.exceptions.runtime.NotFoundException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = PartitionsController.class) +public class PartitionsHandler implements AdviceTrait { + + static final String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; + + @ExceptionHandler(InvalidCursorOperation.class) + public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, + final NativeWebRequest request) { + LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity notFound(final NotFoundException ex, final NativeWebRequest request) { + LOG.error(ex.getMessage(), ex); + return create(Problem.valueOf(NOT_FOUND, ex.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java index 7a4f4c9354..4fee1e7d74 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java @@ -5,8 +5,12 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.nakadi.controller.PostSubscriptionController; +import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.SubscriptionCreationDisabledException; import org.zalando.nakadi.exceptions.runtime.SubscriptionUpdateConflictException; +import org.zalando.nakadi.exceptions.runtime.TooManyPartitionsException; +import org.zalando.nakadi.exceptions.runtime.UnprocessableSubscriptionException; +import org.zalando.nakadi.exceptions.runtime.WrongInitialCursorsException; import org.zalando.problem.Problem; import org.zalando.problem.spring.web.advice.AdviceTrait; @@ -32,4 +36,20 @@ public ResponseEntity handleSubscriptionCreationDisabledException( final NativeWebRequest request) { return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } + + @ExceptionHandler({ + WrongInitialCursorsException.class, + TooManyPartitionsException.class}) + public ResponseEntity handleUnprocessableSubscription(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug("Error occurred when working with subscriptions", exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(UnprocessableSubscriptionException.class) + public ResponseEntity handleUnprocessableSubscriptionException( + final UnprocessableSubscriptionException exception, final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java new file mode 100644 index 0000000000..240f7b1fc2 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java @@ -0,0 +1,27 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.SchemaController; +import org.zalando.nakadi.exceptions.runtime.NoSuchSchemaException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.NOT_FOUND; + +@Priority(10) +@ControllerAdvice(assignableTypes = SchemaController.class) +public class SchemaHandler implements AdviceTrait { + + @ExceptionHandler(NoSuchSchemaException.class) + public ResponseEntity handleNoSuchSchemaException( + final NoSuchSchemaException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java new file mode 100644 index 0000000000..6d40f18a6a --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java @@ -0,0 +1,35 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.SettingsController; +import org.zalando.nakadi.exceptions.runtime.UnableProcessException; +import org.zalando.nakadi.exceptions.runtime.UnknownOperationException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = SettingsController.class) +public class SettingsHandler implements AdviceTrait { + + @ExceptionHandler(UnknownOperationException.class) + public ResponseEntity handleUnknownOperationException(final RuntimeException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, "There was a problem processing your request."), request); + } + + @ExceptionHandler(UnableProcessException.class) + public ResponseEntity handleUnableProcessException(final RuntimeException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java new file mode 100644 index 0000000000..9ce7261b09 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java @@ -0,0 +1,57 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.StoragesController; +import org.zalando.nakadi.exceptions.runtime.DuplicatedStorageException; +import org.zalando.nakadi.exceptions.runtime.NoSuchStorageException; +import org.zalando.nakadi.exceptions.runtime.StorageIsUsedException; +import org.zalando.nakadi.exceptions.runtime.UnknownStorageTypeException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.FORBIDDEN; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = StoragesController.class) +public class StoragesHandler implements AdviceTrait { + + @ExceptionHandler(NoSuchStorageException.class) + public ResponseEntity handleNoSuchStorageException( + final NoSuchStorageException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(StorageIsUsedException.class) + public ResponseEntity handleStorageIsUsedException( + final StorageIsUsedException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); + } + + @ExceptionHandler(DuplicatedStorageException.class) + public ResponseEntity handleDuplicatedStorageException( + final DuplicatedStorageException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(UnknownStorageTypeException.class) + public ResponseEntity handleUnknownStorageTypeException( + final UnknownStorageTypeException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java new file mode 100644 index 0000000000..3a53517a70 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java @@ -0,0 +1,45 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.SubscriptionController; +import org.zalando.nakadi.exceptions.runtime.ErrorGettingCursorTimeLagException; +import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; +import org.zalando.nakadi.exceptions.runtime.TimeLagStatsTimeoutException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.REQUEST_TIMEOUT; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = SubscriptionController.class) +public class SubscriptionHandler implements AdviceTrait { + + @ExceptionHandler(ErrorGettingCursorTimeLagException.class) + public ResponseEntity handleErrorGettingCursorTimeLagException( + final ErrorGettingCursorTimeLagException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(InconsistentStateException.class) + public ResponseEntity handleInconsistentStateExcetpion(final InconsistentStateException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler(TimeLagStatsTimeoutException.class) + public ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException exception, + final NativeWebRequest request) { + LOG.warn(exception.getMessage()); + return create(Problem.valueOf(REQUEST_TIMEOUT, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java new file mode 100644 index 0000000000..224d3544a6 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java @@ -0,0 +1,35 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.SubscriptionStreamController; +import org.zalando.nakadi.exceptions.runtime.NoStreamingSlotsAvailable; +import org.zalando.nakadi.exceptions.runtime.WrongStreamParametersException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = SubscriptionStreamController.class) +public class SubscriptionStreamHandler implements AdviceTrait { + + @ExceptionHandler(WrongStreamParametersException.class) + public ResponseEntity handleWrongStreamParametersException(final WrongStreamParametersException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(NoStreamingSlotsAvailable.class) + public ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java new file mode 100644 index 0000000000..34eff97049 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java @@ -0,0 +1,60 @@ +package org.zalando.nakadi.controller.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.TimelinesController; +import org.zalando.nakadi.exceptions.runtime.ConflictException; +import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; +import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; +import org.zalando.nakadi.exceptions.runtime.NotFoundException; +import org.zalando.nakadi.exceptions.runtime.TimelineException; +import org.zalando.nakadi.exceptions.runtime.TimelinesNotSupportedException; +import org.zalando.nakadi.exceptions.runtime.UnableProcessException; +import org.zalando.problem.Problem; +import org.zalando.problem.spring.web.advice.AdviceTrait; + +import javax.annotation.Priority; + +import static org.zalando.problem.Status.CONFLICT; +import static org.zalando.problem.Status.INTERNAL_SERVER_ERROR; +import static org.zalando.problem.Status.NOT_FOUND; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = TimelinesController.class) +public class TimelinesHandler implements AdviceTrait { + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity notFound(final NotFoundException exception, final NativeWebRequest request) { + LOG.error(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); + } + + @ExceptionHandler(ConflictException.class) + public ResponseEntity handleConflictException(final ConflictException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); + } + + @ExceptionHandler(TimelineException.class) + public ResponseEntity handleTimelineException(final TimelineException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + final Throwable cause = exception.getCause(); + if (cause instanceof InternalNakadiException) { + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); + } + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } + + @ExceptionHandler({UnableProcessException.class, TimelinesNotSupportedException.class}) + public ResponseEntity handleUnprocessableEntityResponse(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage(), exception); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } +} diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java index d51f8d30f5..9bbe8cd597 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java @@ -11,6 +11,7 @@ import org.zalando.nakadi.config.NakadiSettings; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.config.ValidatorConfig; +import org.zalando.nakadi.controller.advice.EventTypeHandler; import org.zalando.nakadi.controller.advice.NakadiProblemHandling; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.EventTypeBase; @@ -119,7 +120,7 @@ public void init() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling()) + .setControllerAdvice(new NakadiProblemHandling(), new EventTypeHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java index 9d0cd8d565..abd0529225 100644 --- a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java @@ -9,6 +9,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.PartitionsHandler; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.NakadiCursor; import org.zalando.nakadi.domain.NakadiCursorLag; @@ -112,7 +113,7 @@ public void before() throws InternalNakadiException, NoSuchEventTypeException { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling()) + .setControllerAdvice(new NakadiProblemHandling(), new PartitionsHandler()) .build(); } From 1bdfdae92f5aa30953083ea5478f55223f609afd Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 16:58:37 +0200 Subject: [PATCH 12/23] Group identical handlers together --- .../advice/NakadiProblemHandling.java | 54 ++++--------------- .../controller/NakadiProblemHandlingTest.java | 2 +- 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java index aec89f4d9a..0126b20a46 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java @@ -92,13 +92,6 @@ public ResponseEntity handleAccessDeniedException(final AccessDeniedExc return create(Problem.valueOf(FORBIDDEN, exception.explain()), request); } - @ExceptionHandler(BlockedException.class) - public ResponseEntity handleBlockedException(final BlockedException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); - } - @ExceptionHandler(DbWriteOperationsBlockedException.class) public ResponseEntity handleDbWriteOperationsBlockedException( final DbWriteOperationsBlockedException exception, final NativeWebRequest request) { @@ -115,12 +108,6 @@ public ResponseEntity handleFeatureNotAvailableException( return create(Problem.valueOf(NOT_IMPLEMENTED, ex.getMessage()), request); } - @ExceptionHandler(IllegalClientIdException.class) - public ResponseEntity handleIllegalClientIdException(final IllegalClientIdException exception, - final NativeWebRequest request) { - return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); - } - @ExceptionHandler(InternalNakadiException.class) public ResponseEntity handleInternalNakadiException(final InternalNakadiException exception, final NativeWebRequest request) { @@ -128,18 +115,9 @@ public ResponseEntity handleInternalNakadiException(final InternalNakad return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } - @ExceptionHandler(InvalidLimitException.class) - public ResponseEntity handleInvalidLimitException( - final InvalidLimitException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); - } - - @ExceptionHandler(InvalidVersionNumberException.class) - public ResponseEntity handleInvalidVersionNumberException( - final InvalidVersionNumberException exception, - final NativeWebRequest request) { + @ExceptionHandler({InvalidLimitException.class, InvalidVersionNumberException.class}) + public ResponseEntity handleBadRequestResponses(final NakadiBaseException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); } @@ -168,23 +146,9 @@ public ResponseEntity handleNakadiRuntimeException(final NakadiRuntimeE throw exception.getException(); } - @ExceptionHandler(NoSuchEventTypeException.class) - public ResponseEntity handleNoSuchEventTypeException(final NoSuchEventTypeException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); - } - - @ExceptionHandler(NoSuchSchemaException.class) - public ResponseEntity handleNoSuchSchemaException(final NoSuchSchemaException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); - } - - @ExceptionHandler(NoSuchSubscriptionException.class) - public ResponseEntity handleNoSuchSubscriptionException(final NoSuchSubscriptionException exception, - final NativeWebRequest request) { + @ExceptionHandler({NoSuchEventTypeException.class, NoSuchSchemaException.class, NoSuchSubscriptionException.class}) + public ResponseEntity handleNotFoundResponses(final NakadiBaseException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } @@ -217,9 +181,9 @@ public ResponseEntity handleValidationException(final ValidationExcepti return create(new ValidationProblem(exception.getErrors()), request); } - @ExceptionHandler(ForbiddenOperationException.class) - public ResponseEntity handleForbiddenOperationException(final ForbiddenOperationException exception, - final NativeWebRequest request) { + @ExceptionHandler({ForbiddenOperationException.class, BlockedException.class, IllegalClientIdException.class}) + public ResponseEntity handleForbiddenResponses(final NakadiBaseException exception, + final NativeWebRequest request) { LOG.error(exception.getMessage()); return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); } diff --git a/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java b/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java index c3daadd0cd..cdc065b6f4 100644 --- a/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java +++ b/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java @@ -21,7 +21,7 @@ public void testIllegalClientIdException() { final NativeWebRequest mockedRequest = Mockito.mock(NativeWebRequest.class); Mockito.when(mockedRequest.getHeader(Matchers.any())).thenReturn(""); - final ResponseEntity problemResponseEntity = nakadiProblemHandling.handleIllegalClientIdException( + final ResponseEntity problemResponseEntity = nakadiProblemHandling.handleForbiddenResponses( new IllegalClientIdException("You don't have access to this event type"), mockedRequest); Assert.assertEquals(problemResponseEntity.getStatusCode(), HttpStatus.FORBIDDEN); From 16ff08c32177283d0300e04140bfc58100e23fe6 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 17:03:52 +0200 Subject: [PATCH 13/23] bump problem version --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a03ae8f200..100d7c1061 100644 --- a/build.gradle +++ b/build.gradle @@ -127,8 +127,8 @@ dependencies { compile('org.zalando.stups:stups-spring-oauth2-server:1.0.22') { exclude module: "httpclient" } - compile 'org.zalando:jackson-datatype-problem:0.21.0' - compile 'org.zalando:problem:0.21.0' + compile 'org.zalando:jackson-datatype-problem:0.22.0' + compile 'org.zalando:problem:0.22.0' compile 'org.zalando:problem-spring-web:0.23.0' compile 'com.google.guava:guava:25.1-jre' compile 'org.slf4j:slf4j-log4j12' From 8dcd1051df8674243bd0e9221a23154b15165c4b Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 17:10:39 +0200 Subject: [PATCH 14/23] Review fix --- .../org/zalando/nakadi/config/SecurityConfiguration.java | 9 +++++---- .../exceptions/runtime/UnknownStatusCodeException.java | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/zalando/nakadi/exceptions/runtime/UnknownStatusCodeException.java diff --git a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java index 3ff91579d2..30c5b4c735 100644 --- a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java +++ b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java @@ -19,6 +19,7 @@ import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.zalando.nakadi.exceptions.runtime.UnknownStatusCodeException; import org.zalando.problem.Status; import org.zalando.problem.StatusType; import org.zalando.stups.oauth2.spring.security.expression.ExtendedOAuth2WebSecurityExpressionHandler; @@ -140,11 +141,11 @@ private static class ProblemOauthMessageConverter extends MappingJackson2HttpMes @Override protected void writeInternal(final Object object, final HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { + throws IOException, HttpMessageNotWritableException, UnknownStatusCodeException { super.writeInternal(toJsonResponse(object), outputMessage); } - protected Object toJsonResponse(final Object object) { + protected Object toJsonResponse(final Object object) throws UnknownStatusCodeException { if (object instanceof OAuth2Exception) { final OAuth2Exception oae = (OAuth2Exception) object; if (oae.getCause() != null) { @@ -192,12 +193,12 @@ public String getDetail() { } } - private static Status fromStatusCode(final int code) { + private static Status fromStatusCode(final int code) throws UnknownStatusCodeException { for (final Status status: Status.values()) { if (status.getStatusCode() == code) { return status; } } - return null; + throw new UnknownStatusCodeException("Unknown status code: " + code); } } diff --git a/src/main/java/org/zalando/nakadi/exceptions/runtime/UnknownStatusCodeException.java b/src/main/java/org/zalando/nakadi/exceptions/runtime/UnknownStatusCodeException.java new file mode 100644 index 0000000000..c254d76ca2 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/exceptions/runtime/UnknownStatusCodeException.java @@ -0,0 +1,8 @@ +package org.zalando.nakadi.exceptions.runtime; + +public class UnknownStatusCodeException extends NakadiBaseException { + + public UnknownStatusCodeException(final String message) { + super(message); + } +} From b1c85fb96eee96099b4a7f2ea45cfda3a0ead727 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 17:21:25 +0200 Subject: [PATCH 15/23] Bump gradle version --- build.gradle | 3 ++- gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 56172 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 100d7c1061..9bd966909f 100644 --- a/build.gradle +++ b/build.gradle @@ -189,7 +189,8 @@ dependencies { // tag::wrapper[] wrapper { - gradleVersion = '4.10' + gradleVersion = '4.10.2' + distributionType = Wrapper.DistributionType.ALL } tasks.withType(FindBugs) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 91ca28c8b802289c3a438766657a5e98f20eff03..28861d273a5d270fd8f65dd74570c17c9c507736 100644 GIT binary patch delta 49415 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%0A>9N>1DoF?DLG9Cx#t>vp)Y=DBpcbo3xSPiI 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 delta 47705 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 786e104107..76b96052c3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Aug 30 16:32:45 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionType=ALL +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip From 38b20ea25aecdc56aa4da984c8a4c3d103fbbd20 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 18:09:13 +0200 Subject: [PATCH 16/23] Add OWASP dependency checker --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 9bd966909f..e70a157d8a 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,7 @@ buildscript { dependencies { classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" classpath 'org.yaml:snakeyaml:1.21' + classpath 'org.owasp:dependency-check-gradle:3.3.2' } } @@ -28,6 +29,7 @@ apply plugin: 'checkstyle' apply plugin: 'project-report' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' +apply plugin: 'org.owasp.dependencycheck' group 'org.zalando' sourceCompatibility = 1.8 From 65228ba3a230f735673a7d1107be73f1a32a3eb0 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Thu, 11 Oct 2018 18:13:32 +0200 Subject: [PATCH 17/23] Bump spring boot version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e70a157d8a..22d89094d8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ import java.util.concurrent.TimeUnit buildscript { ext { - springBootVersion = '1.5.15.RELEASE' + springBootVersion = '1.5.16.RELEASE' } repositories { From 6db5c1690a8afc94345a48b1c686080e46c555a1 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Fri, 12 Oct 2018 10:14:20 +0200 Subject: [PATCH 18/23] Revert spring boot upgrade --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 22d89094d8..e70a157d8a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ import java.util.concurrent.TimeUnit buildscript { ext { - springBootVersion = '1.5.16.RELEASE' + springBootVersion = '1.5.15.RELEASE' } repositories { From e86d256932a076b4b7bc9eb35b866927935f6f13 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Fri, 12 Oct 2018 14:09:50 +0200 Subject: [PATCH 19/23] Fixes after review --- build.gradle | 6 ------ .../advice/CursorOperationsHandler.java | 2 +- .../controller/advice/CursorsHandler.java | 8 ++++---- .../advice/EventPublishingHandler.java | 1 + .../controller/advice/EventTypeHandler.java | 17 +++++------------ .../advice/NakadiProblemHandling.java | 8 ++++---- .../controller/advice/PartitionsHandler.java | 8 ++++---- .../advice/PostSubscriptionHandler.java | 12 ++++-------- .../controller/advice/SettingsHandler.java | 2 +- .../controller/advice/SubscriptionHandler.java | 4 ++-- .../controller/advice/TimelinesHandler.java | 6 +++--- 11 files changed, 29 insertions(+), 45 deletions(-) diff --git a/build.gradle b/build.gradle index e70a157d8a..76a23c3d54 100644 --- a/build.gradle +++ b/build.gradle @@ -189,12 +189,6 @@ dependencies { } // end::dependencies[] -// tag::wrapper[] -wrapper { - gradleVersion = '4.10.2' - distributionType = Wrapper.DistributionType.ALL -} - tasks.withType(FindBugs) { reports { xml.enabled = false diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java index e0b469db1b..1bdb341df8 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java @@ -31,7 +31,7 @@ public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperati @ExceptionHandler(CursorConversionException.class) public ResponseEntity handleCursorConversionException(final CursorConversionException exception, final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java index 63d463c9e4..84c0c96c34 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java @@ -29,14 +29,14 @@ public class CursorsHandler implements AdviceTrait { @ExceptionHandler(UnableProcessException.class) public ResponseEntity handleUnableProcessException(final UnableProcessException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(new ValidationProblem(exception.getBindingResult()), request); } @@ -57,14 +57,14 @@ public ResponseEntity handleInvalidCursorException(final InvalidCursorE @ExceptionHandler(InvalidStreamIdException.class) public ResponseEntity handleInvalidStreamIdException(final InvalidStreamIdException exception, final NativeWebRequest request) { - LOG.warn("Stream id {} is not found: {}", exception.getStreamId(), exception.getMessage()); + LOG.debug("Stream id {} is not found: {}", exception.getStreamId(), exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(RequestInProgressException.class) public ResponseEntity handleRequestInProgressException(final RequestInProgressException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java index 78469dc217..9f9bf50858 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java @@ -27,6 +27,7 @@ public class EventPublishingHandler implements AdviceTrait { @ExceptionHandler(EventTypeTimeoutException.class) public ResponseEntity handleEventTypeTimeoutException(final EventTypeTimeoutException exception, final NativeWebRequest request) { + LOG.error(exception.getMessage()); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java index a66a1b7930..57f4e1929a 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java @@ -29,27 +29,20 @@ @ControllerAdvice(assignableTypes = EventTypeController.class) public class EventTypeHandler implements AdviceTrait { - @ExceptionHandler(NoSuchPartitionStrategyException.class) - public ResponseEntity handleNoSuchPartitionStrategyException( - final NoSuchPartitionStrategyException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - @ExceptionHandler({InvalidEventTypeException.class, UnableProcessException.class, - EventTypeOptionsValidationException.class}) + EventTypeOptionsValidationException.class, + NoSuchPartitionStrategyException.class}) public ResponseEntity handleUnprocessableEntityResponses(final NakadiBaseException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(EventTypeDeletionException.class) public ResponseEntity handleEventTypeDeletionException(final EventTypeDeletionException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } @@ -63,7 +56,7 @@ public ResponseEntity handleConflictResponses(final NakadiBaseException @ExceptionHandler({EventTypeUnavailableException.class, TopicCreationException.class}) public ResponseEntity handleServiceUnavailableResponses(final NakadiBaseException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java index 0126b20a46..072f1e282b 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java @@ -64,7 +64,7 @@ public ResponseEntity handleThrowable(final Throwable throwable, final + errorTraceId + ")"), request); } - public String generateErrorTraceId() { + private String generateErrorTraceId() { return "ETI" + RandomStringUtils.randomAlphanumeric(24); } @@ -111,7 +111,7 @@ public ResponseEntity handleFeatureNotAvailableException( @ExceptionHandler(InternalNakadiException.class) public ResponseEntity handleInternalNakadiException(final InternalNakadiException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } @@ -125,7 +125,7 @@ public ResponseEntity handleBadRequestResponses(final NakadiBaseExcepti @ExceptionHandler(LimitReachedException.class) public ResponseEntity handleLimitReachedException( final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { - LOG.warn(exception.getMessage()); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(TOO_MANY_REQUESTS, exception.getMessage()), request); } @@ -184,7 +184,7 @@ public ResponseEntity handleValidationException(final ValidationExcepti @ExceptionHandler({ForbiddenOperationException.class, BlockedException.class, IllegalClientIdException.class}) public ResponseEntity handleForbiddenResponses(final NakadiBaseException exception, final NativeWebRequest request) { - LOG.error(exception.getMessage()); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java index cdc23d977a..4dcc341456 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java @@ -24,13 +24,13 @@ public class PartitionsHandler implements AdviceTrait { @ExceptionHandler(InvalidCursorOperation.class) public ResponseEntity handleInvalidCursorOperation(final InvalidCursorOperation exception, final NativeWebRequest request) { - LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason(), exception); + LOG.debug("User provided invalid cursor for operation. Reason: " + exception.getReason()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); } @ExceptionHandler(NotFoundException.class) - public ResponseEntity notFound(final NotFoundException ex, final NativeWebRequest request) { - LOG.error(ex.getMessage(), ex); - return create(Problem.valueOf(NOT_FOUND, ex.getMessage()), request); + public ResponseEntity notFound(final NotFoundException exception, final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java index 4fee1e7d74..7eb2244234 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java @@ -27,6 +27,7 @@ public class PostSubscriptionHandler implements AdviceTrait { public ResponseEntity handleSubscriptionUpdateConflictException( final SubscriptionUpdateConflictException exception, final NativeWebRequest request) { + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @@ -34,21 +35,16 @@ public ResponseEntity handleSubscriptionUpdateConflictException( public ResponseEntity handleSubscriptionCreationDisabledException( final SubscriptionCreationDisabledException exception, final NativeWebRequest request) { + LOG.warn(exception.getMessage()); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler({ WrongInitialCursorsException.class, - TooManyPartitionsException.class}) + TooManyPartitionsException.class, + UnprocessableSubscriptionException.class}) public ResponseEntity handleUnprocessableSubscription(final NakadiBaseException exception, final NativeWebRequest request) { - LOG.debug("Error occurred when working with subscriptions", exception); - return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); - } - - @ExceptionHandler(UnprocessableSubscriptionException.class) - public ResponseEntity handleUnprocessableSubscriptionException( - final UnprocessableSubscriptionException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java index 6d40f18a6a..f6d74505ed 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java @@ -29,7 +29,7 @@ public ResponseEntity handleUnknownOperationException(final RuntimeExce @ExceptionHandler(UnableProcessException.class) public ResponseEntity handleUnableProcessException(final RuntimeException exception, final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java index 3a53517a70..ac71e78146 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java @@ -25,14 +25,14 @@ public class SubscriptionHandler implements AdviceTrait { public ResponseEntity handleErrorGettingCursorTimeLagException( final ErrorGettingCursorTimeLagException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } @ExceptionHandler(InconsistentStateException.class) public ResponseEntity handleInconsistentStateExcetpion(final InconsistentStateException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java index 34eff97049..8ca393d009 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java @@ -29,14 +29,14 @@ public class TimelinesHandler implements AdviceTrait { @ExceptionHandler(NotFoundException.class) public ResponseEntity notFound(final NotFoundException exception, final NativeWebRequest request) { - LOG.error(exception.getMessage()); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(NOT_FOUND, exception.getMessage()), request); } @ExceptionHandler(ConflictException.class) public ResponseEntity handleConflictException(final ConflictException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(CONFLICT, exception.getMessage()), request); } @@ -54,7 +54,7 @@ public ResponseEntity handleTimelineException(final TimelineException e @ExceptionHandler({UnableProcessException.class, TimelinesNotSupportedException.class}) public ResponseEntity handleUnprocessableEntityResponse(final NakadiBaseException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); + LOG.debug(exception.getMessage()); return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); } } From 6131ff2396b47c55d19c39fd64508680e9d440f7 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Fri, 12 Oct 2018 14:18:09 +0200 Subject: [PATCH 20/23] Rename controller advices --- ...Handler.java => CursorOperationsExceptionHandler.java} | 2 +- .../{CursorsHandler.java => CursorsExceptionHandler.java} | 2 +- ...gHandler.java => EventPublishingExceptionHandler.java} | 2 +- ...entTypeHandler.java => EventTypeExceptionHandler.java} | 2 +- ...emHandling.java => NakadiProblemExceptionHandler.java} | 4 ++-- ...itionsHandler.java => PartitionsExceptionHandler.java} | 2 +- ...Handler.java => PostSubscriptionExceptionHandler.java} | 2 +- .../{SchemaHandler.java => SchemaExceptionHandler.java} | 2 +- ...SettingsHandler.java => SettingsExceptionHandler.java} | 2 +- ...StoragesHandler.java => StoragesExceptionHandler.java} | 2 +- ...tionHandler.java => SubscriptionExceptionHandler.java} | 2 +- ...ndler.java => SubscriptionStreamExceptionHandler.java} | 2 +- ...melinesHandler.java => TimelinesExceptionHandler.java} | 2 +- .../zalando/nakadi/controller/CursorsControllerTest.java | 6 +++--- .../nakadi/controller/EventPublishingControllerTest.java | 6 +++--- .../nakadi/controller/EventTypeControllerTestCase.java | 6 +++--- ...ngTest.java => NakadiProblemExceptionHandlerTest.java} | 8 ++++---- .../nakadi/controller/PartitionsControllerTest.java | 6 +++--- .../nakadi/controller/PostSubscriptionControllerTest.java | 6 +++--- .../nakadi/controller/SubscriptionControllerTest.java | 6 +++--- .../nakadi/controller/TimelinesControllerTest.java | 4 ++-- 21 files changed, 38 insertions(+), 38 deletions(-) rename src/main/java/org/zalando/nakadi/controller/advice/{CursorOperationsHandler.java => CursorOperationsExceptionHandler.java} (97%) rename src/main/java/org/zalando/nakadi/controller/advice/{CursorsHandler.java => CursorsExceptionHandler.java} (98%) rename src/main/java/org/zalando/nakadi/controller/advice/{EventPublishingHandler.java => EventPublishingExceptionHandler.java} (97%) rename src/main/java/org/zalando/nakadi/controller/advice/{EventTypeHandler.java => EventTypeExceptionHandler.java} (98%) rename src/main/java/org/zalando/nakadi/controller/advice/{NakadiProblemHandling.java => NakadiProblemExceptionHandler.java} (98%) rename src/main/java/org/zalando/nakadi/controller/advice/{PartitionsHandler.java => PartitionsExceptionHandler.java} (96%) rename src/main/java/org/zalando/nakadi/controller/advice/{PostSubscriptionHandler.java => PostSubscriptionExceptionHandler.java} (97%) rename src/main/java/org/zalando/nakadi/controller/advice/{SchemaHandler.java => SchemaExceptionHandler.java} (94%) rename src/main/java/org/zalando/nakadi/controller/advice/{SettingsHandler.java => SettingsExceptionHandler.java} (96%) rename src/main/java/org/zalando/nakadi/controller/advice/{StoragesHandler.java => StoragesExceptionHandler.java} (97%) rename src/main/java/org/zalando/nakadi/controller/advice/{SubscriptionHandler.java => SubscriptionExceptionHandler.java} (97%) rename src/main/java/org/zalando/nakadi/controller/advice/{SubscriptionStreamHandler.java => SubscriptionStreamExceptionHandler.java} (95%) rename src/main/java/org/zalando/nakadi/controller/advice/{TimelinesHandler.java => TimelinesExceptionHandler.java} (97%) rename src/test/java/org/zalando/nakadi/controller/{NakadiProblemHandlingTest.java => NakadiProblemExceptionHandlerTest.java} (79%) diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsExceptionHandler.java similarity index 97% rename from src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsExceptionHandler.java index 1bdb341df8..d297d25583 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsExceptionHandler.java @@ -17,7 +17,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = CursorOperationsController.class) -public class CursorOperationsHandler implements AdviceTrait { +public class CursorOperationsExceptionHandler implements AdviceTrait { @ExceptionHandler(InvalidCursorOperation.class) diff --git a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorsExceptionHandler.java similarity index 98% rename from src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/CursorsExceptionHandler.java index 84c0c96c34..13640a5120 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/CursorsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorsExceptionHandler.java @@ -24,7 +24,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = CursorsController.class) -public class CursorsHandler implements AdviceTrait { +public class CursorsExceptionHandler implements AdviceTrait { @ExceptionHandler(UnableProcessException.class) public ResponseEntity handleUnableProcessException(final UnableProcessException exception, diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingExceptionHandler.java similarity index 97% rename from src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/EventPublishingExceptionHandler.java index 9f9bf50858..870b34a36a 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingExceptionHandler.java @@ -22,7 +22,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = EventPublishingController.class) -public class EventPublishingHandler implements AdviceTrait { +public class EventPublishingExceptionHandler implements AdviceTrait { @ExceptionHandler(EventTypeTimeoutException.class) public ResponseEntity handleEventTypeTimeoutException(final EventTypeTimeoutException exception, diff --git a/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeExceptionHandler.java similarity index 98% rename from src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/EventTypeExceptionHandler.java index 57f4e1929a..b3940363d3 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/EventTypeHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeExceptionHandler.java @@ -27,7 +27,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = EventTypeController.class) -public class EventTypeHandler implements AdviceTrait { +public class EventTypeExceptionHandler implements AdviceTrait { @ExceptionHandler({InvalidEventTypeException.class, UnableProcessException.class, diff --git a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java similarity index 98% rename from src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java rename to src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java index 072f1e282b..07bb1fa20c 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java @@ -46,9 +46,9 @@ @Priority(20) @ControllerAdvice -public class NakadiProblemHandling implements ProblemHandling { +public class NakadiProblemExceptionHandler implements ProblemHandling { - private static final Logger LOG = LoggerFactory.getLogger(NakadiProblemHandling.class); + private static final Logger LOG = LoggerFactory.getLogger(NakadiProblemExceptionHandler.class); @Override public String formatFieldName(final String fieldName) { diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsExceptionHandler.java similarity index 96% rename from src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/PartitionsExceptionHandler.java index 4dcc341456..980bdeb1c6 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/PartitionsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsExceptionHandler.java @@ -17,7 +17,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = PartitionsController.class) -public class PartitionsHandler implements AdviceTrait { +public class PartitionsExceptionHandler implements AdviceTrait { static final String INVALID_CURSOR_MESSAGE = "invalid consumed_offset or partition"; diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionExceptionHandler.java similarity index 97% rename from src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionExceptionHandler.java index 7eb2244234..2f0baa25a2 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionExceptionHandler.java @@ -21,7 +21,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = PostSubscriptionController.class) -public class PostSubscriptionHandler implements AdviceTrait { +public class PostSubscriptionExceptionHandler implements AdviceTrait { @ExceptionHandler(SubscriptionUpdateConflictException.class) public ResponseEntity handleSubscriptionUpdateConflictException( diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SchemaExceptionHandler.java similarity index 94% rename from src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/SchemaExceptionHandler.java index 240f7b1fc2..51685b8bea 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SchemaHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SchemaExceptionHandler.java @@ -15,7 +15,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = SchemaController.class) -public class SchemaHandler implements AdviceTrait { +public class SchemaExceptionHandler implements AdviceTrait { @ExceptionHandler(NoSuchSchemaException.class) public ResponseEntity handleNoSuchSchemaException( diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SettingsExceptionHandler.java similarity index 96% rename from src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/SettingsExceptionHandler.java index f6d74505ed..dba5675baa 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SettingsHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SettingsExceptionHandler.java @@ -17,7 +17,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = SettingsController.class) -public class SettingsHandler implements AdviceTrait { +public class SettingsExceptionHandler implements AdviceTrait { @ExceptionHandler(UnknownOperationException.class) public ResponseEntity handleUnknownOperationException(final RuntimeException exception, diff --git a/src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/StoragesExceptionHandler.java similarity index 97% rename from src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/StoragesExceptionHandler.java index 9ce7261b09..5e535ad62d 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/StoragesHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/StoragesExceptionHandler.java @@ -21,7 +21,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = StoragesController.class) -public class StoragesHandler implements AdviceTrait { +public class StoragesExceptionHandler implements AdviceTrait { @ExceptionHandler(NoSuchStorageException.class) public ResponseEntity handleNoSuchStorageException( diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java similarity index 97% rename from src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java index ac71e78146..99179a9129 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java @@ -19,7 +19,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = SubscriptionController.class) -public class SubscriptionHandler implements AdviceTrait { +public class SubscriptionExceptionHandler implements AdviceTrait { @ExceptionHandler(ErrorGettingCursorTimeLagException.class) public ResponseEntity handleErrorGettingCursorTimeLagException( diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamExceptionHandler.java similarity index 95% rename from src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamExceptionHandler.java index 224d3544a6..1770657ce6 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamExceptionHandler.java @@ -17,7 +17,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = SubscriptionStreamController.class) -public class SubscriptionStreamHandler implements AdviceTrait { +public class SubscriptionStreamExceptionHandler implements AdviceTrait { @ExceptionHandler(WrongStreamParametersException.class) public ResponseEntity handleWrongStreamParametersException(final WrongStreamParametersException exception, diff --git a/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesExceptionHandler.java similarity index 97% rename from src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java rename to src/main/java/org/zalando/nakadi/controller/advice/TimelinesExceptionHandler.java index 8ca393d009..1dfcf85758 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/TimelinesHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesExceptionHandler.java @@ -25,7 +25,7 @@ @Priority(10) @ControllerAdvice(assignableTypes = TimelinesController.class) -public class TimelinesHandler implements AdviceTrait { +public class TimelinesExceptionHandler implements AdviceTrait { @ExceptionHandler(NotFoundException.class) public ResponseEntity notFound(final NotFoundException exception, final NativeWebRequest request) { diff --git a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java index 30f1df3e42..3332446307 100644 --- a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java @@ -8,8 +8,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.zalando.nakadi.config.SecuritySettings; -import org.zalando.nakadi.controller.advice.CursorsHandler; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.CursorsExceptionHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; import org.zalando.nakadi.domain.CursorError; import org.zalando.nakadi.domain.ItemsWrapper; import org.zalando.nakadi.domain.NakadiCursor; @@ -105,7 +105,7 @@ public CursorsControllerTest() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling(), new CursorsHandler()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new CursorsExceptionHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java b/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java index 5241197a39..2856ecd659 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/EventPublishingControllerTest.java @@ -12,8 +12,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.zalando.nakadi.config.SecuritySettings; -import org.zalando.nakadi.controller.advice.EventPublishingHandler; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.EventPublishingExceptionHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; import org.zalando.nakadi.domain.BatchItemResponse; import org.zalando.nakadi.domain.EventPublishResult; import org.zalando.nakadi.domain.EventPublishingStatus; @@ -94,7 +94,7 @@ public void setUp() { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling(), new EventPublishingHandler()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new EventPublishingExceptionHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java index 9bbe8cd597..8fca12851e 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java @@ -11,8 +11,8 @@ import org.zalando.nakadi.config.NakadiSettings; import org.zalando.nakadi.config.SecuritySettings; import org.zalando.nakadi.config.ValidatorConfig; -import org.zalando.nakadi.controller.advice.EventTypeHandler; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.EventTypeExceptionHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.EventTypeBase; import org.zalando.nakadi.domain.Timeline; @@ -120,7 +120,7 @@ public void init() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling(), new EventTypeHandler()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new EventTypeExceptionHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java b/src/test/java/org/zalando/nakadi/controller/NakadiProblemExceptionHandlerTest.java similarity index 79% rename from src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java rename to src/test/java/org/zalando/nakadi/controller/NakadiProblemExceptionHandlerTest.java index cdc065b6f4..35d10497a4 100644 --- a/src/test/java/org/zalando/nakadi/controller/NakadiProblemHandlingTest.java +++ b/src/test/java/org/zalando/nakadi/controller/NakadiProblemExceptionHandlerTest.java @@ -7,21 +7,21 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.NativeWebRequest; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; import org.zalando.problem.Problem; -public class NakadiProblemHandlingTest { +public class NakadiProblemExceptionHandlerTest { private static final String OAUTH2_SCOPE_WRITE = "oauth2.scope.write"; @Test public void testIllegalClientIdException() { - final NakadiProblemHandling nakadiProblemHandling = new NakadiProblemHandling(); + final NakadiProblemExceptionHandler nakadiProblemExceptionHandler = new NakadiProblemExceptionHandler(); final NativeWebRequest mockedRequest = Mockito.mock(NativeWebRequest.class); Mockito.when(mockedRequest.getHeader(Matchers.any())).thenReturn(""); - final ResponseEntity problemResponseEntity = nakadiProblemHandling.handleForbiddenResponses( + final ResponseEntity problemResponseEntity = nakadiProblemExceptionHandler.handleForbiddenResponses( new IllegalClientIdException("You don't have access to this event type"), mockedRequest); Assert.assertEquals(problemResponseEntity.getStatusCode(), HttpStatus.FORBIDDEN); diff --git a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java index abd0529225..57aaaf4992 100644 --- a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java @@ -8,8 +8,8 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.test.web.servlet.MockMvc; import org.zalando.nakadi.config.SecuritySettings; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; -import org.zalando.nakadi.controller.advice.PartitionsHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; +import org.zalando.nakadi.controller.advice.PartitionsExceptionHandler; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.NakadiCursor; import org.zalando.nakadi.domain.NakadiCursorLag; @@ -113,7 +113,7 @@ public void before() throws InternalNakadiException, NoSuchEventTypeException { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(settings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling(), new PartitionsHandler()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new PartitionsExceptionHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java index 1b5ab23a9e..2f38e9072a 100644 --- a/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PostSubscriptionControllerTest.java @@ -14,8 +14,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; -import org.zalando.nakadi.controller.advice.PostSubscriptionHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; +import org.zalando.nakadi.controller.advice.PostSubscriptionExceptionHandler; import org.zalando.nakadi.domain.Subscription; import org.zalando.nakadi.domain.SubscriptionBase; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; @@ -69,7 +69,7 @@ public PostSubscriptionControllerTest() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new PostSubscriptionHandler(), new NakadiProblemHandling()) + .setControllerAdvice(new PostSubscriptionExceptionHandler(), new NakadiProblemExceptionHandler()) .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java index 67ff5b93ae..c0d7b8231a 100644 --- a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java @@ -11,8 +11,8 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import org.zalando.nakadi.config.NakadiSettings; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; -import org.zalando.nakadi.controller.advice.PostSubscriptionHandler; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; +import org.zalando.nakadi.controller.advice.PostSubscriptionExceptionHandler; import org.zalando.nakadi.domain.EventType; import org.zalando.nakadi.domain.EventTypeBase; import org.zalando.nakadi.domain.EventTypePartition; @@ -132,7 +132,7 @@ public SubscriptionControllerTest() throws Exception { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new NakadiProblemHandling(), new PostSubscriptionHandler()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new PostSubscriptionExceptionHandler()) .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java b/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java index 7b646c506d..59bcb52ba6 100644 --- a/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/TimelinesControllerTest.java @@ -11,7 +11,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.zalando.nakadi.config.SecuritySettings; -import org.zalando.nakadi.controller.advice.NakadiProblemHandling; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; import org.zalando.nakadi.domain.Storage; import org.zalando.nakadi.domain.Timeline; import org.zalando.nakadi.security.ClientResolver; @@ -43,7 +43,7 @@ public TimelinesControllerTest() { mockMvc = MockMvcBuilders.standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(securitySettings, featureToggleService)) - .setControllerAdvice(new NakadiProblemHandling()) + .setControllerAdvice(new NakadiProblemExceptionHandler()) .build(); } From 3e2256d3fa88e7e6556488486883c931e417e7ea Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Fri, 12 Oct 2018 16:53:12 +0200 Subject: [PATCH 21/23] Handle 403 consistently --- .../nakadi/controller/StoragesController.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/controller/StoragesController.java b/src/main/java/org/zalando/nakadi/controller/StoragesController.java index 5e320080a7..d943e2fd4f 100644 --- a/src/main/java/org/zalando/nakadi/controller/StoragesController.java +++ b/src/main/java/org/zalando/nakadi/controller/StoragesController.java @@ -12,6 +12,7 @@ import org.zalando.nakadi.domain.Storage; import org.zalando.nakadi.exceptions.runtime.DbWriteOperationsBlockedException; import org.zalando.nakadi.exceptions.runtime.DuplicatedStorageException; +import org.zalando.nakadi.exceptions.runtime.ForbiddenOperationException; import org.zalando.nakadi.exceptions.runtime.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.NoSuchStorageException; import org.zalando.nakadi.exceptions.runtime.StorageIsUsedException; @@ -24,7 +25,6 @@ import java.util.List; import static org.springframework.http.HttpStatus.CREATED; -import static org.springframework.http.HttpStatus.FORBIDDEN; import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.ResponseEntity.status; @@ -43,9 +43,9 @@ public StoragesController(final StorageService storageService, final AdminServic @RequestMapping(value = "/storages", method = RequestMethod.GET) public ResponseEntity listStorages(final NativeWebRequest request) - throws InternalNakadiException, PluginException { + throws InternalNakadiException, PluginException, ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.READ)) { - return status(FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges required to perform this operation"); } final List storages = storageService.listStorages(); return status(OK).body(storages); @@ -55,9 +55,10 @@ public ResponseEntity listStorages(final NativeWebRequest request) public ResponseEntity createStorage(@RequestBody final String storage, final NativeWebRequest request) throws PluginException, DbWriteOperationsBlockedException, - DuplicatedStorageException, InternalNakadiException, UnknownStorageTypeException { + DuplicatedStorageException, InternalNakadiException, UnknownStorageTypeException, + ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return status(FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges required to perform this operation"); } storageService.createStorage(new JSONObject(storage)); return status(CREATED).build(); @@ -65,9 +66,9 @@ public ResponseEntity createStorage(@RequestBody final String storage, @RequestMapping(value = "/storages/{id}", method = RequestMethod.GET) public ResponseEntity getStorage(@PathVariable("id") final String id, final NativeWebRequest request) - throws PluginException, NoSuchStorageException, InternalNakadiException { + throws PluginException, NoSuchStorageException, InternalNakadiException, ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.READ)) { - return status(FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges required to perform this operation"); } final Storage storage = storageService.getStorage(id); return status(OK).body(storage); @@ -76,9 +77,9 @@ public ResponseEntity getStorage(@PathVariable("id") final String id, final N @RequestMapping(value = "/storages/{id}", method = RequestMethod.DELETE) public ResponseEntity deleteStorage(@PathVariable("id") final String id, final NativeWebRequest request) throws PluginException, DbWriteOperationsBlockedException, NoSuchStorageException, - StorageIsUsedException, InternalNakadiException{ + StorageIsUsedException, InternalNakadiException, ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return status(FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges required to perform this operation"); } storageService.deleteStorage(id); return status(NO_CONTENT).build(); @@ -86,9 +87,9 @@ public ResponseEntity deleteStorage(@PathVariable("id") final String id, fina @RequestMapping(value = "/storages/default/{id}", method = RequestMethod.PUT) public ResponseEntity setDefaultStorage(@PathVariable("id") final String id, final NativeWebRequest request) - throws PluginException, NoSuchStorageException, InternalNakadiException { + throws PluginException, NoSuchStorageException, InternalNakadiException, ForbiddenOperationException { if (!adminService.isAdmin(AuthorizationService.Operation.WRITE)) { - return status(FORBIDDEN).build(); + throw new ForbiddenOperationException("Admin privileges required to perform this operation"); } final Storage storage = storageService.setDefaultStorage(id); return status(OK).body(storage); From 85420d7736cdf86e3065adb1e4c733e05a1ca4de Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Fri, 12 Oct 2018 17:20:30 +0200 Subject: [PATCH 22/23] Fix failing test --- .../org/zalando/nakadi/controller/StoragesControllerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java b/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java index 2c6a1cfb40..ffce062e61 100644 --- a/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java @@ -6,6 +6,8 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.test.web.servlet.MockMvc; import org.zalando.nakadi.config.SecuritySettings; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; +import org.zalando.nakadi.controller.advice.SettingsExceptionHandler; import org.zalando.nakadi.domain.Storage; import org.zalando.nakadi.plugin.api.authz.AuthorizationService; import org.zalando.nakadi.security.ClientResolver; @@ -48,6 +50,7 @@ public void before() { mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(securitySettings, featureToggleService)) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new SettingsExceptionHandler()) .build(); } From 3921ea7ea9b7c48eed863b618ac9645acf052f98 Mon Sep 17 00:00:00 2001 From: Lionel Montrieux Date: Tue, 16 Oct 2018 13:35:06 +0200 Subject: [PATCH 23/23] Fix post merge --- .../controller/advice/NakadiProblemExceptionHandler.java | 8 -------- .../controller/advice/SubscriptionExceptionHandler.java | 9 --------- 2 files changed, 17 deletions(-) diff --git a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java index e84af4919e..1bc0506d94 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java @@ -40,7 +40,6 @@ import static org.zalando.problem.Status.NOT_FOUND; import static org.zalando.problem.Status.NOT_IMPLEMENTED; import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; -import static org.zalando.problem.Status.TOO_MANY_REQUESTS; import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @Priority(20) @@ -121,13 +120,6 @@ public ResponseEntity handleBadRequestResponses(final NakadiBaseExcepti return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); } - @ExceptionHandler(LimitReachedException.class) - public ResponseEntity handleLimitReachedException( - final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return create(Problem.valueOf(TOO_MANY_REQUESTS, exception.getMessage()), request); - } - @ExceptionHandler(NakadiBaseException.class) public ResponseEntity handleNakadiBaseException(final NakadiBaseException exception, final NativeWebRequest request) { diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java index 99179a9129..125e3c2e02 100644 --- a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java @@ -7,13 +7,11 @@ import org.zalando.nakadi.controller.SubscriptionController; import org.zalando.nakadi.exceptions.runtime.ErrorGettingCursorTimeLagException; import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; -import org.zalando.nakadi.exceptions.runtime.TimeLagStatsTimeoutException; import org.zalando.problem.Problem; import org.zalando.problem.spring.web.advice.AdviceTrait; import javax.annotation.Priority; -import static org.zalando.problem.Status.REQUEST_TIMEOUT; import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; @@ -35,11 +33,4 @@ public ResponseEntity handleInconsistentStateExcetpion(final Inconsiste LOG.error(exception.getMessage(), exception); return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } - - @ExceptionHandler(TimeLagStatsTimeoutException.class) - public ResponseEntity handleTimeLagStatsTimeoutException(final TimeLagStatsTimeoutException exception, - final NativeWebRequest request) { - LOG.warn(exception.getMessage()); - return create(Problem.valueOf(REQUEST_TIMEOUT, exception.getMessage()), request); - } }