diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java index f923eef693..1718d09e9b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java @@ -1,21 +1,13 @@ package sonia.scm.api.rest; import sonia.scm.AlreadyExistsException; -import sonia.scm.api.v2.resources.ErrorDto; -import sonia.scm.web.VndMediaType; -import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class AlreadyExistsExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(AlreadyExistsException exception) { - return Response.status(Status.CONFLICT) - .entity(ErrorDto.from(exception)) - .type(VndMediaType.ERROR_TYPE) - .build(); +public class AlreadyExistsExceptionMapper extends ContextualExceptionMapper { + public AlreadyExistsExceptionMapper() { + super(AlreadyExistsException.class, Status.CONFLICT); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java index 49aed6a29a..ed2160f8fb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/ConcurrentModificationExceptionMapper.java @@ -1,17 +1,13 @@ package sonia.scm.api.rest; import sonia.scm.ConcurrentModificationException; -import sonia.scm.api.v2.resources.ErrorDto; -import sonia.scm.web.VndMediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class ConcurrentModificationExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(ConcurrentModificationException exception) { - return Response.status(Response.Status.CONFLICT).entity(ErrorDto.from(exception)).type(VndMediaType.ERROR_TYPE).build(); +public class ConcurrentModificationExceptionMapper extends ContextualExceptionMapper { + public ConcurrentModificationExceptionMapper() { + super(ConcurrentModificationException.class, Response.Status.CONFLICT); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/ContextualExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/ContextualExceptionMapper.java new file mode 100644 index 0000000000..7aee62575a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/ContextualExceptionMapper.java @@ -0,0 +1,36 @@ +package sonia.scm.api.rest; + +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.ExceptionWithContext; +import sonia.scm.api.v2.resources.ExceptionWithContextToErrorDtoMapper; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +public class ContextualExceptionMapper implements ExceptionMapper { + + @Inject + private ExceptionWithContextToErrorDtoMapper mapper; + + private static final Logger logger = LoggerFactory.getLogger(ContextualExceptionMapper.class); + + private final Response.Status status; + private final Class type; + + public ContextualExceptionMapper(Class type, Response.Status status) { + this.type = type; + this.status = status; + } + + @Override + public Response toResponse(E exception) { + logger.debug("map {} to status code {}", type.getSimpleName(), status.getStatusCode()); + return Response.status(status) + .entity(mapper.map(exception)) + .type(VndMediaType.ERROR_TYPE) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java index c1f046620a..add41bc3b2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/NotFoundExceptionMapper.java @@ -32,20 +32,17 @@ package sonia.scm.api.v2; import sonia.scm.NotFoundException; -import sonia.scm.api.v2.resources.ErrorDto; -import sonia.scm.web.VndMediaType; +import sonia.scm.api.rest.ContextualExceptionMapper; import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; /** * @since 2.0.0 */ @Provider -public class NotFoundExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(NotFoundException exception) { - return Response.status(Response.Status.NOT_FOUND).entity(ErrorDto.from(exception)).type(VndMediaType.ERROR_TYPE).build(); +public class NotFoundExceptionMapper extends ContextualExceptionMapper { + public NotFoundExceptionMapper() { + super(NotFoundException.class, Response.Status.NOT_FOUND); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java index 8cd2c6d649..4926ed0f66 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java @@ -1,64 +1,26 @@ package sonia.scm.api.v2; -import lombok.Getter; +import com.google.inject.Inject; import org.jboss.resteasy.api.validation.ResteasyViolationException; -import org.slf4j.MDC; -import sonia.scm.api.v2.resources.ErrorDto; +import sonia.scm.api.v2.resources.ViolationExceptionToErrorDtoMapper; -import javax.validation.ConstraintViolation; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import static java.util.Collections.emptyList; @Provider public class ValidationExceptionMapper implements ExceptionMapper { + @Inject + private ViolationExceptionToErrorDtoMapper mapper; + @Override public Response toResponse(ResteasyViolationException exception) { - - List violations = - exception.getConstraintViolations() - .stream() - .map(ConstraintViolationDto::new) - .collect(Collectors.toList()); - return Response .status(Response.Status.BAD_REQUEST) .type(MediaType.APPLICATION_JSON_TYPE) - .entity(new ValidationErrorDto(violations)) + .entity(mapper.map(exception)) .build(); } - - @Getter - public static class ValidationErrorDto extends ErrorDto { - @XmlElement(name = "violation") - @XmlElementWrapper(name = "violations") - private List violations; - - public ValidationErrorDto(List violations) { - super(MDC.get("transaction_id"), "1wR7ZBe7H1", emptyList(), "input violates conditions (see violation list)"); - this.violations = violations; - } - } - - @XmlRootElement(name = "violation") - @Getter - public static class ConstraintViolationDto { - private String path; - private String message; - - public ConstraintViolationDto(ConstraintViolation violation) { - message = violation.getMessage(); - path = violation.getPropertyPath().toString(); - } - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java index df8a33684b..f0df654866 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ErrorDto.java @@ -2,34 +2,32 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Getter; -import org.slf4j.MDC; +import lombok.Setter; import sonia.scm.ContextEntry; -import sonia.scm.ExceptionWithContext; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; import java.util.List; -@Getter +@Getter @Setter public class ErrorDto { - private final String transactionId; - private final String errorCode; - private final List context; - private final String message; + private String transactionId; + private String errorCode; + private List context; + private String message; + + @XmlElement(name = "violation") + @XmlElementWrapper(name = "violations") + private List violations; @JsonInclude(JsonInclude.Include.NON_NULL) - private final String url; + private String url; - protected ErrorDto(String transactionId, String errorCode, List context, String message) { - this(transactionId, errorCode, context, message, null); - } - private ErrorDto(String transactionId, String errorCode, List context, String message, String url) { - this.transactionId = transactionId; - this.errorCode = errorCode; - this.context = context; - this.message = message; - this.url = url; - } - - public static ErrorDto from(ExceptionWithContext exception) { - return new ErrorDto(MDC.get("transaction_id"), exception.getCode(), exception.getContext(), exception.getMessage()); + @XmlRootElement(name = "violation") + @Getter @Setter + public static class ConstraintViolationDto { + private String path; + private String message; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java new file mode 100644 index 0000000000..cdd545542a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ExceptionWithContextToErrorDtoMapper.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.slf4j.MDC; +import sonia.scm.ExceptionWithContext; + +@Mapper +public abstract class ExceptionWithContextToErrorDtoMapper { + + @Mapping(target = "errorCode", source = "code") + @Mapping(target = "transactionId", ignore = true) + @Mapping(target = "violations", ignore = true) + @Mapping(target = "url", ignore = true) + public abstract ErrorDto map(ExceptionWithContext exception); + + @AfterMapping + void setTransactionId(@MappingTarget ErrorDto dto) { + dto.setTransactionId(MDC.get("transaction_id")); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 6497cb9315..35f58cef90 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -39,6 +39,9 @@ public class MapperModule extends AbstractModule { bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); + bind(ViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ViolationExceptionToErrorDtoMapper.class).getClass()); + bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); + // no mapstruct required bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java new file mode 100644 index 0000000000..e713b031f7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java @@ -0,0 +1,54 @@ +package sonia.scm.api.v2.resources; + +import org.jboss.resteasy.api.validation.ResteasyViolationException; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.slf4j.MDC; + +import javax.validation.ConstraintViolation; +import java.util.List; +import java.util.stream.Collectors; + +@Mapper +public abstract class ViolationExceptionToErrorDtoMapper { + + @Mapping(target = "errorCode", ignore = true) + @Mapping(target = "transactionId", ignore = true) + @Mapping(target = "context", ignore = true) + @Mapping(target = "url", ignore = true) + public abstract ErrorDto map(ResteasyViolationException exception); + + @AfterMapping + void setTransactionId(@MappingTarget ErrorDto dto) { + dto.setTransactionId(MDC.get("transaction_id")); + } + + @AfterMapping + void mapViolations(ResteasyViolationException exception, @MappingTarget ErrorDto dto) { + List violations = + exception.getConstraintViolations() + .stream() + .map(this::createViolationDto) + .collect(Collectors.toList()); + dto.setViolations(violations); + } + + private ErrorDto.ConstraintViolationDto createViolationDto(ConstraintViolation violation) { + ErrorDto.ConstraintViolationDto constraintViolationDto = new ErrorDto.ConstraintViolationDto(); + constraintViolationDto.setMessage(violation.getMessage()); + constraintViolationDto.setPath(violation.getPropertyPath().toString()); + return constraintViolationDto; + } + + @AfterMapping + void setErrorCode(@MappingTarget ErrorDto dto) { + dto.setErrorCode("1wR7ZBe7H1"); + } + + @AfterMapping + void setMessage(@MappingTarget ErrorDto dto) { + dto.setMessage("input violates conditions (see violation list)"); + } +}