diff --git a/CHANGELOG.md b/CHANGELOG.md index d50e43a95..277071b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ - `nflow-engine` - When shutdown is requested, stop processing workflows immediately after the current state has been executed. - Add `WorkflowExecutorLister.handlePotentiallyStuck(Duration processingTime)` to support custom handling when nFlow engine thinks the workflow state processing may be stuck. If any registered listener implementation returns true from this method, nFlow will interrupt the processing thread. The default implementation returns false. + - Throw `IllegalArgumentException` instead of `IllegalStateException` when trying to update workflow instance state to an invalid value. + - Throw `IllegalArugmentException` instead of `RuntimeException` when trying to insert workflow instance with unknown type or with a state that is not a start state. + - Make `StateVariableTooLongException` extend `IllegalArgumentException` instead of `RuntimeException`. - Dependency updates: - spring 5.2.5 - jackson 2.10.3 @@ -27,6 +30,10 @@ - spotbugs 4.0.2 - hibernate 6.1.4 - commons-lang3 3.10 +- `nflow-rest-api-jax-rs` + - Convert `IllegalArgumentException` to HTTP Bad Request in `WorkflowInstanceResource.insertWorkflowInstance` and `WorkflowInstanceResource.updateWorkflowInstance`. +- `nflow-rest-api-spring-web` + - Convert `IllegalArgumentException` to HTTP Bad Request in `WorkflowInstanceResource.insertWorkflowInstance` and `WorkflowInstanceResource.updateWorkflowInstance`. - `nflow-explorer` - Dependency updates: - swagger-ui 2.2.10 diff --git a/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowDispatcher.java b/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowDispatcher.java index ebc27ff0f..28de9d6d6 100644 --- a/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowDispatcher.java +++ b/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowDispatcher.java @@ -93,7 +93,7 @@ public void run() { logger.warn(pex.getMessage()); } catch (@SuppressWarnings("unused") InterruptedException dropThrough) { } catch (Exception e) { - logger.error("Exception in executing dispatcher - retrying after sleep period (" + e.getMessage() + ")", e); + logger.error("Exception in executing dispatcher - retrying after sleep period ({})", e.getMessage(), e); sleep(false); } } diff --git a/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowStateProcessor.java b/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowStateProcessor.java index a2ca1e113..90a224df7 100644 --- a/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowStateProcessor.java +++ b/nflow-engine/src/main/java/io/nflow/engine/internal/executor/WorkflowStateProcessor.java @@ -152,7 +152,7 @@ private void runImpl() { WorkflowState state; try { state = definition.getState(instance.state); - } catch (@SuppressWarnings("unused") IllegalStateException e) { + } catch (@SuppressWarnings("unused") IllegalArgumentException e) { rescheduleUnknownWorkflowState(instance); return; } @@ -365,7 +365,7 @@ private void optionallyCleanupWorkflowInstanceHistory(WorkflowSettings settings, maintenanceDao.deleteActionAndStateHistory(instanceId, olderThan); } } catch (Throwable t) { - logger.error("Failure in workflow instance " + instanceId + " history cleanup", t); + logger.error("Failure in workflow instance {} history cleanup", instanceId, t); } } @@ -493,8 +493,8 @@ public NextAction processState() { } } } catch (InvalidNextActionException e) { - logger.error("State '" + instance.state - + "' handler method failed to return valid next action, proceeding to error state '" + errorState + "'", e); + logger.error("State '{}' handler method failed to return valid next action, proceeding to error state '{}'", + instance.state, errorState, e); nextAction = moveToState(errorState, e.getMessage()); execution.setFailed(e); } @@ -522,7 +522,7 @@ private void processBeforeListeners() { try { listener.beforeProcessing(listenerContext); } catch (Throwable t) { - logger.error("Error in " + listener.getClass().getName() + ".beforeProcessing (" + t.getMessage() + ")", t); + logger.error("Error in {}.beforeProcessing ({})", listener.getClass().getName(), t.getMessage(), t); } } } @@ -532,7 +532,7 @@ private void processAfterListeners() { try { listener.afterProcessing(listenerContext); } catch (Throwable t) { - logger.error("Error in " + listener.getClass().getName() + ".afterProcessing (" + t.getMessage() + ")", t); + logger.error("Error in {}.afterProcessing ({})", listener.getClass().getName(), t.getMessage(), t); } } } @@ -542,7 +542,7 @@ private void processAfterFailureListeners(Throwable ex) { try { listener.afterFailure(listenerContext, ex); } catch (Throwable t) { - logger.error("Error in " + listener.getClass().getName() + ".afterFailure (" + t.getMessage() + ")", t); + logger.error("Error in {}.afterFailure ({})", listener.getClass().getName(), t.getMessage(), t); } } } diff --git a/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowDefinitionScanner.java b/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowDefinitionScanner.java index a9c56d616..45aa023e8 100644 --- a/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowDefinitionScanner.java +++ b/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowDefinitionScanner.java @@ -108,7 +108,7 @@ Object defaultValue(StateVar stateInfo, Class clazz) { ctr.newInstance(); return ctr; } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - logger.warn("Could not instantiate " + clazz + " using empty constructor", e); + logger.warn("Could not instantiate {} using empty constructor", clazz, e); } } return null; diff --git a/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowInstancePreProcessor.java b/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowInstancePreProcessor.java index c94c4ff01..4584d9346 100644 --- a/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowInstancePreProcessor.java +++ b/nflow-engine/src/main/java/io/nflow/engine/internal/workflow/WorkflowInstancePreProcessor.java @@ -27,18 +27,17 @@ public WorkflowInstancePreProcessor(WorkflowDefinitionService workflowDefinition this.workflowInstanceDao = workflowInstanceDao; } - // TODO should this set next_activation for child workflows? public WorkflowInstance process(WorkflowInstance instance) { AbstractWorkflowDefinition def = workflowDefinitionService.getWorkflowDefinition(instance.type); if (def == null) { - throw new RuntimeException("No workflow definition found for type [" + instance.type + "]"); + throw new IllegalArgumentException("No workflow definition found for type [" + instance.type + "]"); } WorkflowInstance.Builder builder = new WorkflowInstance.Builder(instance); if (instance.state == null) { builder.setState(def.getInitialState().name()); } else { if (!def.isStartState(instance.state)) { - throw new RuntimeException("Specified state [" + instance.state + "] is not a start state."); + throw new IllegalArgumentException("Specified state [" + instance.state + "] is not a start state."); } } if (isEmpty(instance.externalId)) { diff --git a/nflow-engine/src/main/java/io/nflow/engine/workflow/definition/AbstractWorkflowDefinition.java b/nflow-engine/src/main/java/io/nflow/engine/workflow/definition/AbstractWorkflowDefinition.java index 899e7feb7..7613aff25 100644 --- a/nflow-engine/src/main/java/io/nflow/engine/workflow/definition/AbstractWorkflowDefinition.java +++ b/nflow-engine/src/main/java/io/nflow/engine/workflow/definition/AbstractWorkflowDefinition.java @@ -217,24 +217,26 @@ public WorkflowStateMethod getMethod(String stateName) { /** * Returns the workflow state for the given state name. - * @param state The name of the workflow state. + * + * @param state + * The name of the workflow state. * @return The workflos state matching the state name. - * @throws IllegalStateException when a matching state can not be found. + * @throws IllegalArgumentException + * when a matching state can not be found. */ public WorkflowState getState(String state) { - for (WorkflowState s : getStates()) { - if (Objects.equals(s.name(), state)) { - return s; - } - } - throw new IllegalStateException("No state '" + state + "' in workflow definiton " + getType()); + return getStates().stream().filter(s -> Objects.equals(s.name(), state)).findFirst() + .orElseThrow(() -> new IllegalArgumentException("No state '" + state + "' in workflow definiton " + getType())); } /** * Check if the given state is a valid start state. - * @param state The name of the workflow state. + * + * @param state + * The name of the workflow state. * @return True if the given state is a valid start date, false otherwise. - * @throws IllegalStateException if the given state name does not match any state. + * @throws IllegalArgumentException + * if the given state name does not match any state. */ public boolean isStartState(String state) { return getState(state).getType() == WorkflowStateType.start; diff --git a/nflow-engine/src/main/java/io/nflow/engine/workflow/executor/StateVariableValueTooLongException.java b/nflow-engine/src/main/java/io/nflow/engine/workflow/executor/StateVariableValueTooLongException.java index a04182c47..3bbfa667f 100644 --- a/nflow-engine/src/main/java/io/nflow/engine/workflow/executor/StateVariableValueTooLongException.java +++ b/nflow-engine/src/main/java/io/nflow/engine/workflow/executor/StateVariableValueTooLongException.java @@ -1,6 +1,6 @@ package io.nflow.engine.workflow.executor; -public class StateVariableValueTooLongException extends RuntimeException { +public class StateVariableValueTooLongException extends IllegalArgumentException { private static final long serialVersionUID = 1L; diff --git a/nflow-jetty/src/main/java/io/nflow/jetty/config/NflowJettyConfiguration.java b/nflow-jetty/src/main/java/io/nflow/jetty/config/NflowJettyConfiguration.java index 0eeb07c23..12378fe5b 100644 --- a/nflow-jetty/src/main/java/io/nflow/jetty/config/NflowJettyConfiguration.java +++ b/nflow-jetty/src/main/java/io/nflow/jetty/config/NflowJettyConfiguration.java @@ -36,7 +36,6 @@ import io.nflow.engine.config.NFlow; import io.nflow.jetty.mapper.CustomValidationExceptionMapper; -import io.nflow.jetty.mapper.StateVariableValueTooLongExceptionMapper; import io.nflow.rest.config.RestConfiguration; import io.nflow.rest.config.jaxrs.CorsHeaderContainerResponseFilter; import io.nflow.rest.config.jaxrs.DateTimeParamConverterProvider; @@ -74,11 +73,9 @@ public Server jaxRsServer(WorkflowInstanceResource workflowInstanceResource, } factory.setProviders(asList( jsonProvider(nflowRestObjectMapper), - validationExceptionMapper(), corsHeadersProvider(), new WebApplicationExceptionMapper(), new CustomValidationExceptionMapper(), - new StateVariableValueTooLongExceptionMapper(), new DateTimeParamConverterProvider() )); factory.setFeatures(asList(new LoggingFeature(), swaggerFeature())); @@ -113,11 +110,6 @@ public JacksonJsonProvider jsonProvider(@Named(REST_OBJECT_MAPPER) ObjectMapper return new JacksonJsonProvider(nflowRestObjectMapper); } - @Bean - public CustomValidationExceptionMapper validationExceptionMapper() { - return new CustomValidationExceptionMapper(); - } - @Bean(destroyMethod = "shutdown") public SpringBus cxf() { return new SpringBus(); diff --git a/nflow-jetty/src/main/java/io/nflow/jetty/mapper/StateVariableValueTooLongExceptionMapper.java b/nflow-jetty/src/main/java/io/nflow/jetty/mapper/StateVariableValueTooLongExceptionMapper.java deleted file mode 100644 index e9189e871..000000000 --- a/nflow-jetty/src/main/java/io/nflow/jetty/mapper/StateVariableValueTooLongExceptionMapper.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.nflow.jetty.mapper; - -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; - -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -import io.nflow.engine.workflow.executor.StateVariableValueTooLongException; -import io.nflow.rest.v1.msg.ErrorResponse; - -@Provider -public class StateVariableValueTooLongExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(StateVariableValueTooLongException e) { - return Response.status(BAD_REQUEST).entity(new ErrorResponse(e.getMessage())).build(); - } -} diff --git a/nflow-jetty/src/test/java/io/nflow/jetty/config/NflowJettyConfigurationTest.java b/nflow-jetty/src/test/java/io/nflow/jetty/config/NflowJettyConfigurationTest.java deleted file mode 100644 index 7f8f114c5..000000000 --- a/nflow-jetty/src/test/java/io/nflow/jetty/config/NflowJettyConfigurationTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.nflow.jetty.config; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.notNullValue; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.core.env.Environment; - -@ExtendWith(MockitoExtension.class) -public class NflowJettyConfigurationTest { - - @Mock - private Environment env; - - @Test - public void createsValidationExceptionMapper() { - assertThat(new NflowJettyConfiguration().validationExceptionMapper(), notNullValue()); - } -} diff --git a/nflow-jetty/src/test/java/io/nflow/jetty/mapper/CustomValidationExceptionMapperTest.java b/nflow-jetty/src/test/java/io/nflow/jetty/mapper/CustomValidationExceptionMapperTest.java index 7cfdbb97e..52b15fd38 100644 --- a/nflow-jetty/src/test/java/io/nflow/jetty/mapper/CustomValidationExceptionMapperTest.java +++ b/nflow-jetty/src/test/java/io/nflow/jetty/mapper/CustomValidationExceptionMapperTest.java @@ -1,8 +1,8 @@ package io.nflow.jetty.mapper; import static java.util.Arrays.asList; -import static org.eclipse.jetty.http.HttpStatus.BAD_REQUEST_400; -import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; @@ -48,7 +48,7 @@ public void constraintViolationExceptionCausesBadRequest() { ConstraintViolationException exception = mock(ConstraintViolationException.class); when(exception.getConstraintViolations()).thenReturn(new LinkedHashSet(asList(violation))); try (Response response = exceptionMapper.toResponse(exception)) { - assertThat(response.getStatus(), is(BAD_REQUEST_400)); + assertThat(response.getStatus(), is(BAD_REQUEST.getStatusCode())); ErrorResponse error = (ErrorResponse) response.getEntity(); assertThat(error.error, is("violationPath: violationMessage")); } @@ -59,7 +59,7 @@ public void responseConstraintViolationExceptionCausesInternalServerError() { ConstraintViolationException exception = mock(ResponseConstraintViolationException.class); when(exception.getMessage()).thenReturn("error"); try (Response response = exceptionMapper.toResponse(exception)) { - assertThat(response.getStatus(), is(INTERNAL_SERVER_ERROR_500)); + assertThat(response.getStatus(), is(INTERNAL_SERVER_ERROR.getStatusCode())); ErrorResponse error = (ErrorResponse) response.getEntity(); assertThat(error.error, is("error")); } @@ -70,7 +70,7 @@ public void otherExceptionsCauseInternalServerException() { ValidationException exception = mock(ValidationException.class); when(exception.getMessage()).thenReturn("error"); try (Response response = exceptionMapper.toResponse(exception)) { - assertThat(response.getStatus(), is(INTERNAL_SERVER_ERROR_500)); + assertThat(response.getStatus(), is(INTERNAL_SERVER_ERROR.getStatusCode())); ErrorResponse error = (ErrorResponse) response.getEntity(); assertThat(error.error, is("error")); } diff --git a/nflow-jetty/src/test/java/io/nflow/jetty/mapper/StateVariableValueTooLongExceptionMapperTest.java b/nflow-jetty/src/test/java/io/nflow/jetty/mapper/StateVariableValueTooLongExceptionMapperTest.java deleted file mode 100644 index edc22a7cf..000000000 --- a/nflow-jetty/src/test/java/io/nflow/jetty/mapper/StateVariableValueTooLongExceptionMapperTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.nflow.jetty.mapper; - -import static javax.ws.rs.core.Response.Status.BAD_REQUEST; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import javax.ws.rs.core.Response; - -import org.junit.jupiter.api.Test; - -import io.nflow.engine.workflow.executor.StateVariableValueTooLongException; -import io.nflow.rest.v1.msg.ErrorResponse; - -public class StateVariableValueTooLongExceptionMapperTest { - StateVariableValueTooLongExceptionMapper mapper = new StateVariableValueTooLongExceptionMapper(); - - @Test - public void exceptionIsMappedToBadRequest() { - try (Response response = mapper.toResponse(new StateVariableValueTooLongException("error"))) { - assertThat(response.getStatus(), is(BAD_REQUEST.getStatusCode())); - ErrorResponse error = (ErrorResponse) response.getEntity(); - assertThat(error.error, is("error")); - } - } -} diff --git a/nflow-rest-api-common/src/main/java/io/nflow/rest/v1/msg/SetSignalResponse.java b/nflow-rest-api-common/src/main/java/io/nflow/rest/v1/msg/SetSignalResponse.java new file mode 100644 index 000000000..f77eee92d --- /dev/null +++ b/nflow-rest-api-common/src/main/java/io/nflow/rest/v1/msg/SetSignalResponse.java @@ -0,0 +1,15 @@ +package io.nflow.rest.v1.msg; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.nflow.engine.model.ModelObject; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel(description = "Response to wake up request.") +@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "jackson reads dto fields") +public class SetSignalResponse extends ModelObject { + + @ApiModelProperty("True if the signal was set, false otherwise.") + public boolean setSignalSuccess; + +} diff --git a/nflow-rest-api-jax-rs/src/main/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResource.java b/nflow-rest-api-jax-rs/src/main/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResource.java index ac6df10b8..2abd657d6 100644 --- a/nflow-rest-api-jax-rs/src/main/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResource.java +++ b/nflow-rest-api-jax-rs/src/main/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResource.java @@ -9,7 +9,9 @@ import static javax.ws.rs.core.Response.noContent; import static javax.ws.rs.core.Response.ok; import static javax.ws.rs.core.Response.status; +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.CONFLICT; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; import java.net.URI; import java.util.Collections; @@ -21,7 +23,6 @@ import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; -import javax.ws.rs.NotFoundException; import javax.ws.rs.OPTIONS; import javax.ws.rs.PUT; import javax.ws.rs.Path; @@ -30,10 +31,10 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Component; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.nflow.engine.internal.dao.WorkflowInstanceDao; import io.nflow.engine.service.WorkflowInstanceInclude; import io.nflow.engine.service.WorkflowInstanceService; @@ -47,8 +48,10 @@ import io.nflow.rest.v1.converter.ListWorkflowInstanceConverter; import io.nflow.rest.v1.msg.CreateWorkflowInstanceRequest; import io.nflow.rest.v1.msg.CreateWorkflowInstanceResponse; +import io.nflow.rest.v1.msg.ErrorResponse; import io.nflow.rest.v1.msg.ListWorkflowInstanceResponse; import io.nflow.rest.v1.msg.SetSignalRequest; +import io.nflow.rest.v1.msg.SetSignalResponse; import io.nflow.rest.v1.msg.UpdateWorkflowInstanceRequest; import io.nflow.rest.v1.msg.WakeupRequest; import io.nflow.rest.v1.msg.WakeupResponse; @@ -86,7 +89,7 @@ public WorkflowInstanceResource(WorkflowInstanceService workflowInstances, Creat @ApiOperation(value = "CORS preflight handling") @Consumes(WILDCARD) public Response corsPreflight() { - return Response.ok().build(); + return ok().build(); } @PUT @@ -96,36 +99,44 @@ public Response corsPreflight() { public Response createWorkflowInstance( @Valid @ApiParam(value = "Submitted workflow instance information", required = true) CreateWorkflowInstanceRequest req) { WorkflowInstance instance = createWorkflowConverter.convert(req); - long id = workflowInstances.insertWorkflowInstance(instance); - instance = workflowInstances.getWorkflowInstance(id, EnumSet.of(WorkflowInstanceInclude.CURRENT_STATE_VARIABLES), null); - return created(URI.create(String.valueOf(id))).entity(createWorkflowConverter.convert(instance)).build(); + try { + long id = workflowInstances.insertWorkflowInstance(instance); + instance = workflowInstances.getWorkflowInstance(id, EnumSet.of(WorkflowInstanceInclude.CURRENT_STATE_VARIABLES), null); + return created(URI.create(String.valueOf(id))).entity(createWorkflowConverter.convert(instance)).build(); + } catch (IllegalArgumentException e) { + return status(BAD_REQUEST).entity(new ErrorResponse(e.getMessage())).build(); + } } @PUT @Path("/id/{id}") - @ApiOperation(value = "Update workflow instance", notes = "The service is typically used in manual state " - + "transition via nFlow Explorer or a business UI.") + @ApiOperation(value = "Update workflow instance", notes = "The service is typically used in manual state transition via nFlow Explorer or a business UI.") @ApiResponses({ @ApiResponse(code = 204, message = "If update was successful"), @ApiResponse(code = 400, message = "If instance could not be updated, for example when state variable value was too long"), @ApiResponse(code = 409, message = "If workflow was executing and no update was done") }) public Response updateWorkflowInstance(@ApiParam("Internal id for workflow instance") @PathParam("id") long id, @ApiParam("Submitted workflow instance information") UpdateWorkflowInstanceRequest req) { - boolean updated = super.updateWorkflowInstance(id, req, workflowInstanceFactory, workflowInstances, workflowInstanceDao); - return (updated ? noContent() : status(CONFLICT)).build(); + try { + boolean updated = super.updateWorkflowInstance(id, req, workflowInstanceFactory, workflowInstances, workflowInstanceDao); + return (updated ? noContent() : status(CONFLICT)).build(); + } catch (IllegalArgumentException e) { + return status(BAD_REQUEST).entity(new ErrorResponse(e.getMessage())).build(); + } } @GET @Path("/id/{id}") @ApiOperation(value = "Fetch a workflow instance", notes = "Fetch full state and action history of a single workflow instance.") + @ApiResponses({ @ApiResponse(code = 200, response = ListWorkflowInstanceResponse.class, message = "If instance was found"), + @ApiResponse(code = 404, message = "If instance was not found") }) @SuppressFBWarnings(value = "LEST_LOST_EXCEPTION_STACK_TRACE", justification = "The empty result exception contains no useful information") - public ListWorkflowInstanceResponse fetchWorkflowInstance( - @ApiParam("Internal id for workflow instance") @PathParam("id") long id, + public Response fetchWorkflowInstance(@ApiParam("Internal id for workflow instance") @PathParam("id") long id, @QueryParam("include") @ApiParam(value = INCLUDE_PARAM_DESC, allowableValues = INCLUDE_PARAM_VALUES, allowMultiple = true) String include, @QueryParam("maxActions") @ApiParam("Maximum number of actions returned for each workflow instance") Long maxActions) { try { - return super.fetchWorkflowInstance(id, include, maxActions, workflowInstances, listWorkflowConverter); + return ok(super.fetchWorkflowInstance(id, include, maxActions, workflowInstances, listWorkflowConverter)).build(); } catch (@SuppressWarnings("unused") EmptyResultDataAccessException e) { - throw new NotFoundException(format("Workflow instance %s not found", id)); + return status(NOT_FOUND).entity(new ErrorResponse(format("Workflow instance %s not found", id))).build(); } } @@ -151,11 +162,12 @@ public Iterator listWorkflowInstances( @PUT @Path("/{id}/signal") @ApiOperation(value = "Set workflow instance signal value", notes = "The service may be used for example to interrupt executing workflow instance.") - @ApiResponses({ @ApiResponse(code = 200, message = "When operation was successful") }) - public Response setSignal(@ApiParam("Internal id for workflow instance") @PathParam("id") long id, + @ApiResponses({ @ApiResponse(code = 200, message = "When setting the signal was attempted") }) + public SetSignalResponse setSignal(@ApiParam("Internal id for workflow instance") @PathParam("id") long id, @Valid @ApiParam("New signal value") SetSignalRequest req) { - boolean updated = workflowInstances.setSignal(id, ofNullable(req.signal), req.reason, WorkflowActionType.externalChange); - return (updated ? ok("Signal was set successfully") : ok("Signal was not set")).build(); + SetSignalResponse response = new SetSignalResponse(); + response.setSignalSuccess = workflowInstances.setSignal(id, ofNullable(req.signal), req.reason, WorkflowActionType.externalChange); + return response; } @PUT diff --git a/nflow-rest-api-jax-rs/src/test/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResourceTest.java b/nflow-rest-api-jax-rs/src/test/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResourceTest.java index 1ecd1eb24..8afa307c1 100644 --- a/nflow-rest-api-jax-rs/src/test/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResourceTest.java +++ b/nflow-rest-api-jax-rs/src/test/java/io/nflow/rest/v1/jaxrs/WorkflowInstanceResourceTest.java @@ -5,13 +5,15 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptySet; import static javax.ws.rs.core.Response.Status.CREATED; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; @@ -25,7 +27,6 @@ import java.util.Optional; import java.util.Set; -import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import org.joda.time.DateTime; @@ -54,8 +55,10 @@ import io.nflow.rest.v1.converter.CreateWorkflowConverter; import io.nflow.rest.v1.converter.ListWorkflowInstanceConverter; import io.nflow.rest.v1.msg.CreateWorkflowInstanceRequest; +import io.nflow.rest.v1.msg.ErrorResponse; import io.nflow.rest.v1.msg.ListWorkflowInstanceResponse; import io.nflow.rest.v1.msg.SetSignalRequest; +import io.nflow.rest.v1.msg.SetSignalResponse; import io.nflow.rest.v1.msg.UpdateWorkflowInstanceRequest; @ExtendWith(MockitoExtension.class) @@ -146,7 +149,7 @@ public void whenUpdatingStateWithDescriptionUpdateWorkflowInstanceWorks() { @Test public void whenUpdatingNextActivationTimeUpdateWorkflowInstanceWorks() { UpdateWorkflowInstanceRequest req = new UpdateWorkflowInstanceRequest(); - req.nextActivationTime = new DateTime(2014,11,12,17,55,0); + req.nextActivationTime = new DateTime(2014, 11, 12, 17, 55, 0); resource.updateWorkflowInstance(3, req); verify(workflowInstances).updateWorkflowInstance( (WorkflowInstance) argThat(allOf(hasField("state", equalTo(null)), hasField("status", equalTo(null)))), @@ -222,10 +225,12 @@ public void listWorkflowInstancesWorksWithAllIncludes() { } @Test - public void fetchingNonExistingWorkflowThrowsNotFoundException() { - when(workflowInstances.getWorkflowInstance(42, emptySet(), null)) - .thenThrow(EmptyResultDataAccessException.class); - assertThrows(NotFoundException.class, () -> resource.fetchWorkflowInstance(42, null, null)); + public void fetchingNonExistingWorkflowReturnsNotFound() { + when(workflowInstances.getWorkflowInstance(42, emptySet(), null)).thenThrow(EmptyResultDataAccessException.class); + try (Response response = resource.fetchWorkflowInstance(42, null, null)) { + assertThat(response.getStatus(), is(equalTo(NOT_FOUND.getStatusCode()))); + assertThat(response.readEntity(ErrorResponse.class).error, is(equalTo("Workflow instance 42 not found"))); + } } @SuppressWarnings("unchecked") @@ -235,7 +240,7 @@ public void fetchingExistingWorkflowWorks() { when(workflowInstances.getWorkflowInstance(42, emptySet(), null)).thenReturn(instance); ListWorkflowInstanceResponse resp = mock(ListWorkflowInstanceResponse.class); when(listWorkflowConverter.convert(eq(instance), any(Set.class))).thenReturn(resp); - ListWorkflowInstanceResponse result = resource.fetchWorkflowInstance(42, null, null); + ListWorkflowInstanceResponse result = resource.fetchWorkflowInstance(42, null, null).readEntity(ListWorkflowInstanceResponse.class); verify(workflowInstances).getWorkflowInstance(42, emptySet(), null); assertEquals(resp, result); } @@ -249,37 +254,34 @@ public void fetchingExistingWorkflowWorksWithAllIncludes() { ListWorkflowInstanceResponse resp = mock(ListWorkflowInstanceResponse.class); when(listWorkflowConverter.convert(eq(instance), any(Set.class))).thenReturn(resp); ListWorkflowInstanceResponse result = resource.fetchWorkflowInstance(42, - "actions,currentStateVariables,actionStateVariables,childWorkflows", 10L); + "actions,currentStateVariables,actionStateVariables,childWorkflows", 10L).readEntity(ListWorkflowInstanceResponse.class); verify(workflowInstances).getWorkflowInstance(42, includes, 10L); assertEquals(resp, result); } @Test - public void setSignalWorks() { + public void setSignalSuccessIsTrueWhenSignalWasSet() { SetSignalRequest req = new SetSignalRequest(); req.signal = 42; req.reason = "testing"; when(workflowInstances.setSignal(99, Optional.of(42), "testing", WorkflowActionType.externalChange)).thenReturn(true); - try (Response response = resource.setSignal(99, req)) { - verify(workflowInstances).setSignal(99, Optional.of(42), "testing", WorkflowActionType.externalChange); - assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); - assertThat(response.readEntity(String.class), is("Signal was set successfully")); - } + SetSignalResponse response = resource.setSignal(99, req); + + verify(workflowInstances).setSignal(99, Optional.of(42), "testing", WorkflowActionType.externalChange); + assertTrue(response.setSignalSuccess); } @Test - public void setSignalReturnsOkWhenSignalIsNotUpdated() { + public void setSignalSuccessIsFalseWhenSignalWasNotSet() { SetSignalRequest req = new SetSignalRequest(); req.signal = null; req.reason = "testing"; when(workflowInstances.setSignal(99, Optional.empty(), "testing", WorkflowActionType.externalChange)).thenReturn(false); - try (Response response = resource.setSignal(99, req)) { - verify(workflowInstances).setSignal(99, Optional.empty(), "testing", WorkflowActionType.externalChange); - assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); - assertThat(response.readEntity(String.class), is("Signal was not set")); - } - } + SetSignalResponse response = resource.setSignal(99, req); + verify(workflowInstances).setSignal(99, Optional.empty(), "testing", WorkflowActionType.externalChange); + assertFalse(response.setSignalSuccess); + } } diff --git a/nflow-rest-api-spring-web/src/main/java/io/nflow/rest/v1/springweb/WorkflowInstanceResource.java b/nflow-rest-api-spring-web/src/main/java/io/nflow/rest/v1/springweb/WorkflowInstanceResource.java index fe6c65f4a..6cea1c7f4 100644 --- a/nflow-rest-api-spring-web/src/main/java/io/nflow/rest/v1/springweb/WorkflowInstanceResource.java +++ b/nflow-rest-api-spring-web/src/main/java/io/nflow/rest/v1/springweb/WorkflowInstanceResource.java @@ -2,12 +2,14 @@ import static io.nflow.rest.config.springweb.PathConstants.NFLOW_SPRING_WEB_PATH_PREFIX; import static io.nflow.rest.v1.ResourcePaths.NFLOW_WORKFLOW_INSTANCE_PATH; +import static java.lang.String.format; import static java.util.Optional.ofNullable; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.ResponseEntity.created; import static org.springframework.http.ResponseEntity.noContent; -import static org.springframework.http.ResponseEntity.notFound; import static org.springframework.http.ResponseEntity.ok; import static org.springframework.http.ResponseEntity.status; @@ -20,7 +22,6 @@ import javax.inject.Inject; import javax.validation.Valid; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.nflow.engine.internal.dao.WorkflowInstanceDao; import io.nflow.engine.service.WorkflowInstanceInclude; import io.nflow.engine.service.WorkflowInstanceService; @@ -44,8 +46,10 @@ import io.nflow.rest.v1.converter.ListWorkflowInstanceConverter; import io.nflow.rest.v1.msg.CreateWorkflowInstanceRequest; import io.nflow.rest.v1.msg.CreateWorkflowInstanceResponse; +import io.nflow.rest.v1.msg.ErrorResponse; import io.nflow.rest.v1.msg.ListWorkflowInstanceResponse; import io.nflow.rest.v1.msg.SetSignalRequest; +import io.nflow.rest.v1.msg.SetSignalResponse; import io.nflow.rest.v1.msg.UpdateWorkflowInstanceRequest; import io.nflow.rest.v1.msg.WakeupRequest; import io.nflow.rest.v1.msg.WakeupResponse; @@ -81,12 +85,16 @@ public WorkflowInstanceResource(WorkflowInstanceService workflowInstances, Creat @ApiOperation(value = "Submit new workflow instance") @ApiResponses({ @ApiResponse(code = 201, message = "Workflow was created", response = CreateWorkflowInstanceResponse.class), @ApiResponse(code = 400, message = "If instance could not be created, for example when state variable value was too long") }) - public ResponseEntity createWorkflowInstance( + public ResponseEntity createWorkflowInstance( @RequestBody @ApiParam(value = "Submitted workflow instance information", required = true) CreateWorkflowInstanceRequest req) { WorkflowInstance instance = createWorkflowConverter.convert(req); - long id = workflowInstances.insertWorkflowInstance(instance); - instance = workflowInstances.getWorkflowInstance(id, EnumSet.of(WorkflowInstanceInclude.CURRENT_STATE_VARIABLES), null); - return created(URI.create(String.valueOf(id))).body(createWorkflowConverter.convert(instance)); + try { + long id = workflowInstances.insertWorkflowInstance(instance); + instance = workflowInstances.getWorkflowInstance(id, EnumSet.of(WorkflowInstanceInclude.CURRENT_STATE_VARIABLES), null); + return created(URI.create(String.valueOf(id))).body(createWorkflowConverter.convert(instance)); + } catch (IllegalArgumentException e) { + return status(BAD_REQUEST).body(new ErrorResponse((e.getMessage()))); + } } @PutMapping(path = "/id/{id}", consumes = APPLICATION_JSON_VALUE) @@ -97,21 +105,27 @@ public ResponseEntity createWorkflowInstance( @ApiResponse(code = 409, message = "If workflow was executing and no update was done") }) public ResponseEntity updateWorkflowInstance(@ApiParam("Internal id for workflow instance") @PathVariable("id") long id, @RequestBody @ApiParam("Submitted workflow instance information") UpdateWorkflowInstanceRequest req) { - boolean updated = super.updateWorkflowInstance(id, req, workflowInstanceFactory, workflowInstances, workflowInstanceDao); - return (updated ? noContent() : status(CONFLICT)).build(); + try { + boolean updated = super.updateWorkflowInstance(id, req, workflowInstanceFactory, workflowInstances, workflowInstanceDao); + return (updated ? noContent() : status(CONFLICT)).build(); + } catch (IllegalArgumentException e) { + return status(BAD_REQUEST).body(new ErrorResponse(e.getMessage())); + } } @GetMapping(path = "/id/{id}") @ApiOperation(value = "Fetch a workflow instance", notes = "Fetch full state and action history of a single workflow instance.") + @ApiResponses({ @ApiResponse(code = 200, response = ListWorkflowInstanceResponse.class, message = "If instance was found"), + @ApiResponse(code = 404, message = "If instance was not found") }) @SuppressFBWarnings(value = "LEST_LOST_EXCEPTION_STACK_TRACE", justification = "The empty result exception contains no useful information") - public ResponseEntity fetchWorkflowInstance( + public ResponseEntity fetchWorkflowInstance( @ApiParam("Internal id for workflow instance") @PathVariable("id") long id, @RequestParam(value = "include", required = false) @ApiParam(value = INCLUDE_PARAM_DESC, allowableValues = INCLUDE_PARAM_VALUES, allowMultiple = true) String include, @RequestParam(value = "maxActions", required = false) @ApiParam("Maximum number of actions returned for each workflow instance") Long maxActions) { try { return ok().body(super.fetchWorkflowInstance(id, include, maxActions, this.workflowInstances, this.listWorkflowConverter)); } catch (@SuppressWarnings("unused") EmptyResultDataAccessException e) { - return notFound().build(); + return status(NOT_FOUND).body(new ErrorResponse(format("Workflow instance %s not found", id))); } } @@ -135,11 +149,13 @@ public Iterator listWorkflowInstances( @PutMapping(path = "/{id}/signal", consumes = APPLICATION_JSON_VALUE) @ApiOperation(value = "Set workflow instance signal value", notes = "The service may be used for example to interrupt executing workflow instance.") - @ApiResponses({ @ApiResponse(code = 200, message = "When operation was successful") }) - public ResponseEntity setSignal(@ApiParam("Internal id for workflow instance") @PathVariable("id") long id, + @ApiResponses({ @ApiResponse(code = 200, message = "When setting the signal was attempted") }) + public SetSignalResponse setSignal(@ApiParam("Internal id for workflow instance") @PathVariable("id") long id, @RequestBody @Valid @ApiParam("New signal value") SetSignalRequest req) { - boolean updated = workflowInstances.setSignal(id, ofNullable(req.signal), req.reason, WorkflowActionType.externalChange); - return (updated ? ok("Signal was set successfully") : ok("Signal was not set")); + SetSignalResponse response = new SetSignalResponse(); + response.setSignalSuccess = workflowInstances.setSignal(id, ofNullable(req.signal), req.reason, + WorkflowActionType.externalChange); + return response; } @PutMapping(path = "/{id}/wakeup", consumes = APPLICATION_JSON_VALUE) diff --git a/nflow-tests/src/test/java/io/nflow/tests/AbstractNflowTest.java b/nflow-tests/src/test/java/io/nflow/tests/AbstractNflowTest.java index d767fc0ef..c81785039 100644 --- a/nflow-tests/src/test/java/io/nflow/tests/AbstractNflowTest.java +++ b/nflow-tests/src/test/java/io/nflow/tests/AbstractNflowTest.java @@ -33,6 +33,7 @@ import io.nflow.rest.v1.msg.MaintenanceRequest.MaintenanceRequestItem; import io.nflow.rest.v1.msg.MaintenanceResponse; import io.nflow.rest.v1.msg.SetSignalRequest; +import io.nflow.rest.v1.msg.SetSignalResponse; import io.nflow.rest.v1.msg.StatisticsResponse; import io.nflow.rest.v1.msg.UpdateWorkflowInstanceRequest; import io.nflow.rest.v1.msg.WakeupRequest; @@ -100,11 +101,11 @@ protected WakeupResponse wakeup(long instanceId, List expectedStates) { return getInstanceResource(instanceId).path("wakeup").put(request, WakeupResponse.class); } - protected String setSignal(long instanceId, int signal, String reason) { + protected SetSignalResponse setSignal(long instanceId, int signal, String reason) { SetSignalRequest request = new SetSignalRequest(); request.signal = signal; request.reason = reason; - return getInstanceResource(instanceId).path("signal").put(request, String.class); + return getInstanceResource(instanceId).path("signal").put(request, SetSignalResponse.class); } private WebClient getInstanceResource(long instanceId) { diff --git a/nflow-tests/src/test/java/io/nflow/tests/CreditApplicationWorkflowTest.java b/nflow-tests/src/test/java/io/nflow/tests/CreditApplicationWorkflowTest.java index 9d90eec1f..3534ac1ca 100644 --- a/nflow-tests/src/test/java/io/nflow/tests/CreditApplicationWorkflowTest.java +++ b/nflow-tests/src/test/java/io/nflow/tests/CreditApplicationWorkflowTest.java @@ -3,14 +3,19 @@ import static io.nflow.engine.workflow.instance.WorkflowInstanceAction.WorkflowActionType.stateExecution; import static java.time.Duration.ofSeconds; import static java.util.Arrays.asList; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; import static org.apache.cxf.jaxrs.client.WebClient.fromClient; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; import static org.joda.time.DateTime.now; import java.math.BigDecimal; import java.util.UUID; +import javax.ws.rs.core.Response; + import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -21,6 +26,7 @@ import io.nflow.rest.v1.msg.Action; import io.nflow.rest.v1.msg.CreateWorkflowInstanceRequest; import io.nflow.rest.v1.msg.CreateWorkflowInstanceResponse; +import io.nflow.rest.v1.msg.ErrorResponse; import io.nflow.rest.v1.msg.UpdateWorkflowInstanceRequest; import io.nflow.tests.demo.workflow.CreditApplicationWorkflow; import io.nflow.tests.extension.NflowServerConfig; @@ -67,12 +73,25 @@ public void moveToGrantLoanState() { @Test @Order(4) + public void moveToInvalidStateFails() { + UpdateWorkflowInstanceRequest ureq = new UpdateWorkflowInstanceRequest(); + ureq.nextActivationTime = now(); + ureq.state = "invalid"; + try (Response response = fromClient(workflowInstanceIdResource, true).path(resp.id).put(ureq)) { + assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode())); + assertThat(response.getMediaType(), is(APPLICATION_JSON_TYPE)); + assertThat(response.readEntity(ErrorResponse.class).error, startsWith("No state 'invalid'")); + } + } + + @Test + @Order(5) public void checkErrorStateReached() { getWorkflowInstanceWithTimeout(resp.id, "error", ofSeconds(5)); } @Test - @Order(5) + @Order(6) public void checkWorkflowInstanceActions() { int i = 1; assertWorkflowInstance(resp.id, actionHistoryValidator(asList( diff --git a/nflow-tests/src/test/java/io/nflow/tests/SignalWorkflowTest.java b/nflow-tests/src/test/java/io/nflow/tests/SignalWorkflowTest.java index b8c8835d7..4b4eea049 100644 --- a/nflow-tests/src/test/java/io/nflow/tests/SignalWorkflowTest.java +++ b/nflow-tests/src/test/java/io/nflow/tests/SignalWorkflowTest.java @@ -6,9 +6,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import io.nflow.tests.extension.NflowServerConfig; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -22,6 +22,7 @@ import io.nflow.rest.v1.msg.ListWorkflowInstanceResponse; import io.nflow.tests.demo.workflow.DemoWorkflow; import io.nflow.tests.demo.workflow.SlowWorkflow; +import io.nflow.tests.extension.NflowServerConfig; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class SignalWorkflowTest extends AbstractNflowTest { @@ -65,8 +66,7 @@ public void checkSlowWorkflowIsRunning() throws Exception { @Test @Order(3) public void interruptWorkflowWithSignal() { - assertThat(setSignal(resp.id, SlowWorkflow.SIGNAL_INTERRUPT, "Setting signal via REST API"), - is("Signal was set successfully")); + assertTrue(setSignal(resp.id, SlowWorkflow.SIGNAL_INTERRUPT, "Setting signal via REST API").setSignalSuccess); } @Test diff --git a/nflow-tests/src/test/java/io/nflow/tests/StateVariablesTest.java b/nflow-tests/src/test/java/io/nflow/tests/StateVariablesTest.java index 73a10dc8a..ee0850156 100644 --- a/nflow-tests/src/test/java/io/nflow/tests/StateVariablesTest.java +++ b/nflow-tests/src/test/java/io/nflow/tests/StateVariablesTest.java @@ -2,6 +2,7 @@ import static java.time.Duration.ofSeconds; import static java.util.Collections.singletonMap; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static org.apache.commons.lang3.StringUtils.repeat; import static org.apache.cxf.jaxrs.client.WebClient.fromClient; @@ -109,6 +110,7 @@ public void updateWorkflowWithTooLongStateVariableValueReturnsBadRequest() { try (Response response = getInstanceIdResource(createResponse.id).put(req)) { assertThat(response.getStatus(), is(BAD_REQUEST.getStatusCode())); + assertThat(response.getMediaType(), is(APPLICATION_JSON_TYPE)); assertThat(response.readEntity(ErrorResponse.class).error, startsWith("Too long value")); } } @@ -123,6 +125,7 @@ public void insertWorkflowWithTooLongStateVariableValueReturnsBadRequest() { try (Response response = fromClient(workflowInstanceResource, true).put(createRequest)) { assertThat(response.getStatus(), is(BAD_REQUEST.getStatusCode())); + assertThat(response.getMediaType(), is(APPLICATION_JSON_TYPE)); assertThat(response.readEntity(ErrorResponse.class).error, startsWith("Too long value")); } }