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..76a23c3d54 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 @@ -127,9 +129,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.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' compile "io.dropwizard.metrics:metrics-core:$dropwizardVersion" @@ -187,11 +189,6 @@ dependencies { } // end::dependencies[] -// tag::wrapper[] -task wrapper(type: Wrapper) { - gradleVersion = '2.3' -} - tasks.withType(FindBugs) { reports { xml.enabled = false @@ -265,7 +262,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() } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 91ca28c8b8..28861d273a 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7dc503f149..76b96052c3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip +distributionType=ALL +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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/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 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 4a9d1c3812..a4fcbfa1fa 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 { @@ -473,7 +473,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"))); } @@ -493,7 +493,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 012995bd59..30c5b4c735 100644 --- a/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java +++ b/src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java @@ -19,9 +19,11 @@ 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; -import javax.ws.rs.core.Response; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -31,6 +33,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 @@ -137,24 +141,24 @@ 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) { 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"); } } @@ -165,7 +169,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(); @@ -189,4 +193,12 @@ public String getDetail() { } } + private static Status fromStatusCode(final int code) throws UnknownStatusCodeException { + for (final Status status: Status.values()) { + if (status.getStatusCode() == code) { + return status; + } + } + throw new UnknownStatusCodeException("Unknown status code: " + code); + } } diff --git a/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java b/src/main/java/org/zalando/nakadi/controller/CursorOperationsController.java index 9c9267fbc6..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; @@ -43,7 +37,6 @@ import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.ResponseEntity.status; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; @RestController public class CursorOperationsController { @@ -133,35 +126,6 @@ public List cursorsLag(@PathVariable("eventTypeName") final String ev .collect(Collectors.toList()); } - @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); - } - - @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); - } - - 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 0802be8ad7..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; @@ -15,44 +13,29 @@ 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; 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; -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; @RestController public class CursorsController { @@ -60,17 +43,14 @@ public class CursorsController { 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; } @@ -92,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)); } } @@ -128,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( @@ -151,46 +115,4 @@ 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); - } - - @ExceptionHandler(UnableProcessException.class) - public ResponseEntity handleUnableProcessException(final RuntimeException 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); - } - - @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) { - LOG.debug(ex.getMessage(), ex); - return Responses.create(Problem.valueOf(Response.Status.NOT_IMPLEMENTED, "Feature is disabled"), 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); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java b/src/main/java/org/zalando/nakadi/controller/EventPublishingController.java index ac6ef46382..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; @@ -9,7 +8,6 @@ import org.springframework.beans.factory.annotation.Value; 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; @@ -18,13 +16,10 @@ 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.EnrichmentException; +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.InvalidPartitionKeyFieldsException; -import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; -import org.zalando.nakadi.exceptions.runtime.PartitioningException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; import org.zalando.nakadi.metrics.EventTypeMetricRegistry; import org.zalando.nakadi.metrics.EventTypeMetrics; @@ -32,21 +27,14 @@ 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 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.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; @RestController public class EventPublishingController { @@ -78,43 +66,36 @@ public ResponseEntity postEvent(@PathVariable final String eventTypeName, @RequestBody final String eventsAsString, final NativeWebRequest request, final Client client) - throws AccessDeniedException, EnrichmentException, - PartitioningException, ServiceTemporarilyUnavailableException { + 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 Responses.create( - Problem.valueOf(Response.Status.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(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + eventTypeMetrics.incrementResponseCount(INTERNAL_SERVER_ERROR.getStatusCode()); throw ex; } } - @ExceptionHandler({EnrichmentException.class, - PartitioningException.class, - InvalidPartitionKeyFieldsException.class}) - public ResponseEntity handleUnprocessableEntityResponses(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - private ResponseEntity postEventInternal(final String eventTypeName, final String eventsAsString, final NativeWebRequest nativeWebRequest, final EventTypeMetrics eventTypeMetrics, final Client client) - throws AccessDeniedException, EnrichmentException, PartitioningException, - ServiceTemporarilyUnavailableException { + throws AccessDeniedException, ServiceTemporarilyUnavailableException, InternalNakadiException, + EventTypeTimeoutException, NoSuchEventTypeException { final long startingNanos = System.nanoTime(); try { final EventPublishResult result = publisher.publish(eventsAsString, eventTypeName); @@ -126,18 +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); - return processJSONException(e, nativeWebRequest); - } catch (final NoSuchEventTypeException e) { - LOG.debug("Event type not found.", e.getMessage()); - return create(Problem.valueOf(NOT_FOUND, e.getMessage()), nativeWebRequest); - } 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); } finally { eventTypeMetrics.updateTiming(startingNanos, System.nanoTime()); } @@ -176,17 +145,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(Response.Status.BAD_REQUEST), nativeWebRequest); - } - - private ThrowableProblem createProblem(final JSONException e) { - return Problem.valueOf(Response.Status.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/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 155de667b1..9a09e6abce 100644 --- a/src/main/java/org/zalando/nakadi/controller/EventTypeController.java +++ b/src/main/java/org/zalando/nakadi/controller/EventTypeController.java @@ -1,14 +1,11 @@ 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; 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,12 +20,11 @@ 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.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; -import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; import org.zalando.nakadi.exceptions.runtime.NoSuchPartitionStrategyException; @@ -36,46 +32,36 @@ 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.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; -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 { - 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; } @@ -92,13 +78,14 @@ public ResponseEntity create(@Valid @RequestBody final EventTypeBase eventTyp final Errors errors, final NativeWebRequest request) throws TopicCreationException, InternalNakadiException, NoSuchPartitionStrategyException, - DuplicatedEventTypeNameException, InvalidEventTypeException { + 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()) { - return Responses.create(new ValidationProblem(errors), request); + throw new ValidationException(errors); } eventTypeService.create(eventType); @@ -115,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); @@ -134,9 +121,10 @@ public ResponseEntity update( NakadiRuntimeException, ServiceTemporarilyUnavailableException, UnableProcessException, - NoSuchPartitionStrategyException{ + NoSuchPartitionStrategyException, + ValidationException { if (errors.hasErrors()) { - return Responses.create(new ValidationProblem(errors), request); + throw new ValidationException(errors); } eventTypeService.update(name, eventType); @@ -170,35 +158,4 @@ private HttpHeaders generateWarningHeaders(final EventTypeBase eventType) { return headers; } - - @ExceptionHandler({UnableProcessException.class, - NoSuchPartitionStrategyException.class, - InvalidEventTypeException.class, - EventTypeOptionsValidationException.class}) - public ResponseEntity handleUnprocessableEntityResponses(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); - } - - @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({ConflictException.class, DuplicatedEventTypeNameException.class}) - public ResponseEntity handleConflictResponses(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Response.Status.CONFLICT, exception.getMessage(), request); - } - - @ExceptionHandler({EventTypeUnavailableException.class, TopicCreationException.class}) - public ResponseEntity handleServiceUnavailableResponses(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create(Response.Status.SERVICE_UNAVAILABLE, 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/PartitionsController.java b/src/main/java/org/zalando/nakadi/controller/PartitionsController.java index 19e37ac713..7b607eb3f7 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,21 +31,14 @@ 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; @RestController public class PartitionsController { @@ -57,7 +48,6 @@ public class PartitionsController { 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; @@ -74,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) @@ -116,42 +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(MoreStatus.UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), - request); + return ok().body(result); } - } - @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) @@ -201,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 8a66fb26c8..baf663e3f9 100644 --- a/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/PostSubscriptionController.java @@ -1,12 +1,9 @@ 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; 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,26 +15,18 @@ 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.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.nakadi.plugin.api.ApplicationService; -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.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; @@ -45,35 +34,33 @@ @RestController public class PostSubscriptionController { - 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; } @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 Responses.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 Responses.create(Response.Status.SERVICE_UNAVAILABLE, - "Subscription creation is temporarily unavailable", request); + throw new SubscriptionCreationDisabledException("Subscription creation is temporarily unavailable"); } try { final Subscription subscription = subscriptionService.createSubscription(subscriptionBase); @@ -91,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 Responses.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); - } catch (final NoSuchSubscriptionException ex) { - return Responses.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) { @@ -114,17 +97,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, - UnprocessableSubscriptionException.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/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/SettingsController.java b/src/main/java/org/zalando/nakadi/controller/SettingsController.java index 6b0db0317b..3f41fda00c 100644 --- a/src/main/java/org/zalando/nakadi/controller/SettingsController.java +++ b/src/main/java/org/zalando/nakadi/controller/SettingsController.java @@ -1,33 +1,23 @@ 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; -import org.springframework.web.bind.annotation.ExceptionHandler; +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.exceptions.runtime.UnableProcessException; -import org.zalando.nakadi.exceptions.runtime.UnknownOperationException; +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; -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; @@ -35,37 +25,34 @@ @RequestMapping(value = "/settings") public class SettingsController { - private static final Logger LOG = LoggerFactory.getLogger(SettingsController.class); 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; } @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(); @@ -73,65 +60,53 @@ 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) { + public ResponseEntity updateAdmins(@Valid @RequestBody final ResourceAuthorization authz, + 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); } adminService.updateAdmins(authz.toPermissionsList(ADMIN_RESOURCE)); 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/StoragesController.java b/src/main/java/org/zalando/nakadi/controller/StoragesController.java index 5ea02b3fa9..d943e2fd4f 100644 --- a/src/main/java/org/zalando/nakadi/controller/StoragesController.java +++ b/src/main/java/org/zalando/nakadi/controller/StoragesController.java @@ -1,21 +1,18 @@ package org.zalando.nakadi.controller; import org.json.JSONObject; -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.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.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,43 +21,31 @@ import org.zalando.nakadi.plugin.api.authz.AuthorizationService; import org.zalando.nakadi.service.AdminService; import org.zalando.nakadi.service.StorageService; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; -import javax.ws.rs.core.Response; import java.util.List; -import static javax.ws.rs.core.Response.Status.CONFLICT; -import static javax.ws.rs.core.Response.Status.NOT_FOUND; 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; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; @RestController public class StoragesController { - private static final Logger LOG = LoggerFactory.getLogger(StoragesController.class); - - 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; } @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); @@ -70,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(); @@ -80,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); @@ -91,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(); @@ -101,43 +87,11 @@ 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); } - - @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(Response.Status.FORBIDDEN, 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); - } } diff --git a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java index 4bb9ccd1e6..73242840a2 100644 --- a/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java +++ b/src/main/java/org/zalando/nakadi/controller/SubscriptionController.java @@ -1,10 +1,7 @@ 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.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -14,45 +11,31 @@ 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.ErrorGettingCursorTimeLagException; -import org.zalando.nakadi.exceptions.runtime.FeatureNotAvailableException; 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.NakadiBaseException; 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; -import org.zalando.problem.Problem; -import org.zalando.problem.spring.web.advice.Responses; import javax.annotation.Nullable; 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 { - 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; } @@ -95,29 +78,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, ServiceTemporarilyUnavailableException.class}) - public ResponseEntity handleServiceUnavailableResponses(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage(), exception); - return Responses.create( - Problem.valueOf( - SERVICE_UNAVAILABLE, - exception.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 ab8de46013..506f5541a4 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; @@ -31,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; @@ -40,7 +36,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,13 +45,13 @@ 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 { @@ -64,7 +59,6 @@ public class SubscriptionStreamController { 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; @@ -75,7 +69,6 @@ public class SubscriptionStreamController { @Autowired public SubscriptionStreamController(final SubscriptionStreamerFactory subscriptionStreamerFactory, - final FeatureToggleService featureToggleService, final ObjectMapper objectMapper, final ClosedConnectionsCrutch closedConnectionsCrutch, final NakadiSettings nakadiSettings, @@ -84,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; @@ -245,19 +237,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); - } - - @ExceptionHandler(NoStreamingSlotsAvailable.class) - public ResponseEntity handleNoStreamingSlotsAvailable(final NoStreamingSlotsAvailable exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(CONFLICT, 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 8bdc559bc0..179ca52561 100644 --- a/src/main/java/org/zalando/nakadi/controller/TimelinesController.java +++ b/src/main/java/org/zalando/nakadi/controller/TimelinesController.java @@ -1,46 +1,30 @@ 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; -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.InternalNakadiException; -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 javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @RestController -@RequestMapping(value = "/event-types/{name}/timelines", produces = MediaType.APPLICATION_JSON) +@RequestMapping(value = "/event-types/{name}/timelines", produces = APPLICATION_JSON_VALUE) public class TimelinesController { - private static final Logger LOG = LoggerFactory.getLogger(TimelinesController.class); - private final TimelineService timelineService; @Autowired @@ -63,35 +47,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); - } - - @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); - } } 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/controller/advice/CursorOperationsExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsExceptionHandler.java new file mode 100644 index 0000000000..d297d25583 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorOperationsExceptionHandler.java @@ -0,0 +1,51 @@ +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.CursorConversionException; +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 CursorOperationsExceptionHandler 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); + } + + @ExceptionHandler(CursorConversionException.class) + public ResponseEntity handleCursorConversionException(final CursorConversionException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + 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 " + + "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/CursorsExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/CursorsExceptionHandler.java new file mode 100644 index 0000000000..13640a5120 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/CursorsExceptionHandler.java @@ -0,0 +1,77 @@ +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.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; +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.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; + +@Priority(10) +@ControllerAdvice(assignableTypes = CursorsController.class) +public class CursorsExceptionHandler implements AdviceTrait { + + @ExceptionHandler(UnableProcessException.class) + public ResponseEntity handleUnableProcessException(final UnableProcessException exception, + final NativeWebRequest request) { + 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()); + return create(new ValidationProblem(exception.getBindingResult()), request); + } + + @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.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()); + 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/EventPublishingExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingExceptionHandler.java new file mode 100644 index 0000000000..870b34a36a --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventPublishingExceptionHandler.java @@ -0,0 +1,53 @@ +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.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; + +import javax.annotation.Priority; + +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) +public class EventPublishingExceptionHandler 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); + } + + @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); + } + + @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/EventTypeExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeExceptionHandler.java new file mode 100644 index 0000000000..b3940363d3 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/EventTypeExceptionHandler.java @@ -0,0 +1,62 @@ +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 EventTypeExceptionHandler implements AdviceTrait { + + @ExceptionHandler({InvalidEventTypeException.class, + UnableProcessException.class, + EventTypeOptionsValidationException.class, + NoSuchPartitionStrategyException.class}) + public ResponseEntity handleUnprocessableEntityResponses(final NakadiBaseException exception, + final NativeWebRequest request) { + 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.error(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.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java similarity index 50% rename from src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java rename to src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.java index a41f58054f..1bc0506d94 100644 --- a/src/main/java/org/zalando/nakadi/controller/ExceptionHandling.java +++ b/src/main/java/org/zalando/nakadi/controller/advice/NakadiProblemExceptionHandler.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; @@ -11,8 +11,10 @@ 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.DbWriteOperationsBlockedException; 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.InternalNakadiException; import org.zalando.nakadi.exceptions.runtime.InvalidLimitException; @@ -20,29 +22,31 @@ import org.zalando.nakadi.exceptions.runtime.NakadiBaseException; import org.zalando.nakadi.exceptions.runtime.NakadiRuntimeException; import org.zalando.nakadi.exceptions.runtime.NoSuchEventTypeException; +import org.zalando.nakadi.exceptions.runtime.NoSuchSchemaException; import org.zalando.nakadi.exceptions.runtime.NoSuchSubscriptionException; import org.zalando.nakadi.exceptions.runtime.RepositoryProblemException; import org.zalando.nakadi.exceptions.runtime.ServiceTemporarilyUnavailableException; import org.zalando.nakadi.exceptions.runtime.UnprocessableEntityException; +import org.zalando.nakadi.exceptions.runtime.ValidationException; +import org.zalando.nakadi.problem.ValidationProblem; 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.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 javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE; -import static org.zalando.problem.MoreStatus.UNPROCESSABLE_ENTITY; +import javax.annotation.Priority; +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.NOT_IMPLEMENTED; +import static org.zalando.problem.Status.SERVICE_UNAVAILABLE; +import static org.zalando.problem.Status.UNPROCESSABLE_ENTITY; +@Priority(20) @ControllerAdvice -public final class ExceptionHandling implements ProblemHandling { +public class NakadiProblemExceptionHandler implements ProblemHandling { - private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandling.class); + private static final Logger LOG = LoggerFactory.getLogger(NakadiProblemExceptionHandler.class); @Override public String formatFieldName(final String fieldName) { @@ -54,8 +58,8 @@ public String formatFieldName(final String fieldName) { 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); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, "An internal error happened. Please report it. (" + + errorTraceId + ")"), request); } private String generateErrorTraceId() { @@ -77,72 +81,81 @@ class and stacktrace like information. } else { message = exception.getMessage(); } - return Responses.create(Response.Status.BAD_REQUEST, message, request); + return create(Problem.valueOf(BAD_REQUEST, message), request); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handleAccessDeniedException(final AccessDeniedException exception, final NativeWebRequest request) { - return Responses.create(FORBIDDEN, exception.explain(), request); - } - @ExceptionHandler(IllegalClientIdException.class) - public ResponseEntity handleForbiddenRequests(final NakadiBaseException exception, - final NativeWebRequest request) { - return Responses.create(FORBIDDEN, exception.getMessage(), request); - } - - @ExceptionHandler({RepositoryProblemException.class, ServiceTemporarilyUnavailableException.class}) - public ResponseEntity handleServiceUnavailableResponses(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.error(exception.getMessage(), exception); - return Responses.create(SERVICE_UNAVAILABLE, exception.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(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); + return create(Problem.valueOf(FORBIDDEN, exception.explain()), 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); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, + "Database is currently in read-only mode"), request); } @ExceptionHandler(FeatureNotAvailableException.class) - public ResponseEntity handleFeatureNotAvailable( + public ResponseEntity handleFeatureNotAvailableException( final FeatureNotAvailableException ex, final NativeWebRequest request) { LOG.debug(ex.getMessage()); - return Responses.create(Problem.valueOf(NOT_IMPLEMENTED, ex.getMessage()), request); + return create(Problem.valueOf(NOT_IMPLEMENTED, ex.getMessage()), request); } - @ExceptionHandler({NoSuchEventTypeException.class, NoSuchSubscriptionException.class}) - public ResponseEntity handleNotFoundRequests(final NakadiBaseException exception, - final NativeWebRequest request) { - LOG.debug(exception.getMessage()); - return Responses.create(NOT_FOUND, exception.getMessage(), request); + @ExceptionHandler(InternalNakadiException.class) + public ResponseEntity handleInternalNakadiException(final InternalNakadiException exception, + final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(INTERNAL_SERVER_ERROR, exception.getMessage()), request); } @ExceptionHandler({InvalidLimitException.class, InvalidVersionNumberException.class}) - public ResponseEntity handleBadRequests(final NakadiBaseException exception, - final NativeWebRequest request) { + public ResponseEntity handleBadRequestResponses(final NakadiBaseException exception, + final NativeWebRequest request) { LOG.debug(exception.getMessage()); - return Responses.create(BAD_REQUEST, exception.getMessage(), request); + return create(Problem.valueOf(BAD_REQUEST, exception.getMessage()), request); + } + + @ExceptionHandler(NakadiBaseException.class) + 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 + 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); + } + throw exception.getException(); + } + + @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); + } + + @ExceptionHandler(RepositoryProblemException.class) + 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(ServiceTemporarilyUnavailableException.class) + public ResponseEntity handleServiceTemporarilyUnavailableException( + final ServiceTemporarilyUnavailableException exception, final NativeWebRequest request) { + LOG.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); } @ExceptionHandler(UnprocessableEntityException.class) @@ -150,6 +163,19 @@ public ResponseEntity handleUnprocessableEntityException( final UnprocessableEntityException exception, final NativeWebRequest request) { LOG.debug(exception.getMessage()); - return Responses.create(UNPROCESSABLE_ENTITY, exception.getMessage(), request); + 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); + } + + @ExceptionHandler({ForbiddenOperationException.class, BlockedException.class, IllegalClientIdException.class}) + public ResponseEntity handleForbiddenResponses(final NakadiBaseException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(FORBIDDEN, exception.getMessage()), request); } } diff --git a/src/main/java/org/zalando/nakadi/controller/advice/PartitionsExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsExceptionHandler.java new file mode 100644 index 0000000000..980bdeb1c6 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/PartitionsExceptionHandler.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 PartitionsExceptionHandler 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()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, INVALID_CURSOR_MESSAGE), request); + } + + @ExceptionHandler(NotFoundException.class) + 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/PostSubscriptionExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionExceptionHandler.java new file mode 100644 index 0000000000..2f0baa25a2 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/PostSubscriptionExceptionHandler.java @@ -0,0 +1,51 @@ +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.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; + +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 PostSubscriptionExceptionHandler implements AdviceTrait { + + @ExceptionHandler(SubscriptionUpdateConflictException.class) + public ResponseEntity handleSubscriptionUpdateConflictException( + final SubscriptionUpdateConflictException exception, + final NativeWebRequest request) { + LOG.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } + + @ExceptionHandler(SubscriptionCreationDisabledException.class) + 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, + UnprocessableSubscriptionException.class}) + public ResponseEntity handleUnprocessableSubscription(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/SchemaExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SchemaExceptionHandler.java new file mode 100644 index 0000000000..51685b8bea --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SchemaExceptionHandler.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 SchemaExceptionHandler 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/SettingsExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SettingsExceptionHandler.java new file mode 100644 index 0000000000..dba5675baa --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SettingsExceptionHandler.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 SettingsExceptionHandler 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.debug(exception.getMessage()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/StoragesExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/StoragesExceptionHandler.java new file mode 100644 index 0000000000..5e535ad62d --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/StoragesExceptionHandler.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 StoragesExceptionHandler 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/SubscriptionExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.java new file mode 100644 index 0000000000..125e3c2e02 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionExceptionHandler.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.SubscriptionController; +import org.zalando.nakadi.exceptions.runtime.ErrorGettingCursorTimeLagException; +import org.zalando.nakadi.exceptions.runtime.InconsistentStateException; +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 = SubscriptionController.class) +public class SubscriptionExceptionHandler implements AdviceTrait { + + @ExceptionHandler(ErrorGettingCursorTimeLagException.class) + public ResponseEntity handleErrorGettingCursorTimeLagException( + final ErrorGettingCursorTimeLagException exception, + final NativeWebRequest request) { + 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.error(exception.getMessage(), exception); + return create(Problem.valueOf(SERVICE_UNAVAILABLE, exception.getMessage()), request); + } +} diff --git a/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamExceptionHandler.java new file mode 100644 index 0000000000..1770657ce6 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/SubscriptionStreamExceptionHandler.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 SubscriptionStreamExceptionHandler 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/TimelinesExceptionHandler.java b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesExceptionHandler.java new file mode 100644 index 0000000000..1dfcf85758 --- /dev/null +++ b/src/main/java/org/zalando/nakadi/controller/advice/TimelinesExceptionHandler.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 TimelinesExceptionHandler implements AdviceTrait { + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity notFound(final NotFoundException exception, final NativeWebRequest request) { + 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()); + 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()); + return create(Problem.valueOf(UNPROCESSABLE_ENTITY, 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/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); + } +} 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/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); + } +} 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/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..3bd26c5674 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 { @@ -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; } diff --git a/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java b/src/test/java/org/zalando/nakadi/controller/CursorsControllerTest.java index 810f287dd3..3332446307 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.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; @@ -33,8 +35,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 +49,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 { @@ -94,8 +96,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(); @@ -104,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 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 65749fce4d..2856ecd659 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.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; @@ -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 NakadiProblemExceptionHandler(), new EventPublishingExceptionHandler()) .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/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 7c964714c1..ef467b68c0 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 whenDELETENotAuthorizedThen403() 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..8fca12851e 100644 --- a/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java +++ b/src/test/java/org/zalando/nakadi/controller/EventTypeControllerTestCase.java @@ -11,6 +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.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; @@ -110,7 +112,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()); @@ -118,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 ExceptionHandling()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new EventTypeExceptionHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/ExceptionHandlingTest.java b/src/test/java/org/zalando/nakadi/controller/NakadiProblemExceptionHandlerTest.java similarity index 74% rename from src/test/java/org/zalando/nakadi/controller/ExceptionHandlingTest.java rename to src/test/java/org/zalando/nakadi/controller/NakadiProblemExceptionHandlerTest.java index 7ea059c9b1..35d10497a4 100644 --- a/src/test/java/org/zalando/nakadi/controller/ExceptionHandlingTest.java +++ b/src/test/java/org/zalando/nakadi/controller/NakadiProblemExceptionHandlerTest.java @@ -7,24 +7,25 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.nakadi.controller.advice.NakadiProblemExceptionHandler; import org.zalando.nakadi.exceptions.runtime.IllegalClientIdException; import org.zalando.problem.Problem; -public class ExceptionHandlingTest { +public class NakadiProblemExceptionHandlerTest { private static final String OAUTH2_SCOPE_WRITE = "oauth2.scope.write"; @Test public void testIllegalClientIdException() { - final ExceptionHandling exceptionHandling = new ExceptionHandling(); + final NakadiProblemExceptionHandler nakadiProblemExceptionHandler = new NakadiProblemExceptionHandler(); final NativeWebRequest mockedRequest = Mockito.mock(NativeWebRequest.class); Mockito.when(mockedRequest.getHeader(Matchers.any())).thenReturn(""); - final ResponseEntity problemResponseEntity = exceptionHandling.handleForbiddenRequests( + final ResponseEntity problemResponseEntity = nakadiProblemExceptionHandler.handleForbiddenResponses( 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..57aaaf4992 100644 --- a/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java +++ b/src/test/java/org/zalando/nakadi/controller/PartitionsControllerTest.java @@ -8,6 +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.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; @@ -38,8 +40,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 +51,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 { @@ -94,7 +96,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 +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 ExceptionHandling()) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new PartitionsExceptionHandler()) .build(); } @@ -144,7 +146,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()) @@ -233,7 +235,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( @@ -262,7 +265,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..2f38e9072a 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.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; @@ -38,14 +40,14 @@ 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 { 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); @@ -63,12 +65,13 @@ public PostSubscriptionControllerTest() throws Exception { when(subscriptionService.getSubscriptionUri(any())).thenCallRealMethod(); final PostSubscriptionController controller = new PostSubscriptionController(featureToggleService, - applicationService, subscriptionService); + subscriptionService); - mockMvcBuilder = standaloneSetup(controller) + mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new ExceptionHandling()) - .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()); + .setControllerAdvice(new PostSubscriptionExceptionHandler(), new NakadiProblemExceptionHandler()) + .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/StoragesControllerTest.java b/src/test/java/org/zalando/nakadi/controller/StoragesControllerTest.java index 48cae8d611..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; @@ -41,13 +43,14 @@ 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(); mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) .setCustomArgumentResolvers(new ClientResolver(securitySettings, featureToggleService)) + .setControllerAdvice(new NakadiProblemExceptionHandler(), new SettingsExceptionHandler()) .build(); } diff --git a/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java b/src/test/java/org/zalando/nakadi/controller/SubscriptionControllerTest.java index d31eefb27d..c0d7b8231a 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.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; @@ -50,7 +52,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 +62,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 +81,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 { @@ -93,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; @@ -125,14 +126,15 @@ 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()); - mockMvcBuilder = standaloneSetup(controller) + mockMvc = standaloneSetup(controller) .setMessageConverters(new StringHttpMessageConverter(), TestUtils.JACKSON_2_HTTP_MESSAGE_CONVERTER) - .setControllerAdvice(new ExceptionHandling()) - .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()); + .setControllerAdvice(new NakadiProblemExceptionHandler(), new PostSubscriptionExceptionHandler()) + .setCustomArgumentResolvers(new TestHandlerMethodArgumentResolver()) + .build(); } @Test @@ -150,7 +152,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()) @@ -286,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()); } @@ -314,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")); } @@ -328,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 7c3f9e20cc..59bcb52ba6 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.NakadiProblemExceptionHandler; 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 ExceptionHandling()) + .setControllerAdvice(new NakadiProblemExceptionHandler()) .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) {