From b9458f47e9550901ed6c1f852b7960d0ea44915c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 20 Nov 2018 11:04:30 +0100 Subject: [PATCH 1/5] Add support for custom violation exceptions --- .../scm/ScmConstraintViolationException.java | 76 +++++++++++++++++++ ...=> ResteasyValidationExceptionMapper.java} | 8 +- ...cmConstraintValidationExceptionMapper.java | 30 ++++++++ .../scm/api/v2/resources/MapperModule.java | 3 +- ...syViolationExceptionToErrorDtoMapper.java} | 2 +- ...ScmViolationExceptionToErrorDtoMapper.java | 53 +++++++++++++ 6 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java rename scm-webapp/src/main/java/sonia/scm/api/v2/{ValidationExceptionMapper.java => ResteasyValidationExceptionMapper.java} (62%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{ViolationExceptionToErrorDtoMapper.java => ResteasyViolationExceptionToErrorDtoMapper.java} (96%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java new file mode 100644 index 0000000000..1fb31dc0fc --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -0,0 +1,76 @@ +package sonia.scm; + +import java.util.ArrayList; +import java.util.Collection; + +import static java.util.Collections.unmodifiableCollection; + +public class ScmConstraintViolationException extends RuntimeException { + + private final Collection violations; + + private final String furtherInformations; + + private ScmConstraintViolationException(Collection violations, String furtherInformations) { + this.violations = violations; + this.furtherInformations = furtherInformations; + } + + public Collection getViolations() { + return unmodifiableCollection(violations); + } + + public String getUrl() { + return furtherInformations; + } + + public static class Builder { + private final Collection violations = new ArrayList<>(); + private String furtherInformations; + + public static Builder doThrow() { + Builder builder = new Builder(); + return builder; + } + + public Builder andThrow() { + this.violations.clear(); + this.furtherInformations = null; + return this; + } + + public Builder violation(String message, String... pathElements) { + this.violations.add(new ScmConstraintViolation(message, pathElements)); + return this; + } + + public Builder withFurtherInformations(String furtherInformations) { + this.furtherInformations = furtherInformations; + return this; + } + + public void when(boolean condition) { + if (condition && !this.violations.isEmpty()) { + throw new ScmConstraintViolationException(violations, furtherInformations); + } + } + } + + public static class ScmConstraintViolation { + private final String message; + private final String path; + + private ScmConstraintViolation(String message, String... pathElements) { + this.message = message; + this.path = String.join(".", pathElements); + } + + public String getMessage() { + return message; + } + + public String getPropertyPath() { + return path; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java similarity index 62% rename from scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java index 6fadce8500..63582e10b8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java @@ -1,7 +1,7 @@ package sonia.scm.api.v2; import org.jboss.resteasy.api.validation.ResteasyViolationException; -import sonia.scm.api.v2.resources.ViolationExceptionToErrorDtoMapper; +import sonia.scm.api.v2.resources.ResteasyViolationExceptionToErrorDtoMapper; import javax.inject.Inject; import javax.ws.rs.core.MediaType; @@ -10,12 +10,12 @@ import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider -public class ValidationExceptionMapper implements ExceptionMapper { +public class ResteasyValidationExceptionMapper implements ExceptionMapper { - private final ViolationExceptionToErrorDtoMapper mapper; + private final ResteasyViolationExceptionToErrorDtoMapper mapper; @Inject - public ValidationExceptionMapper(ViolationExceptionToErrorDtoMapper mapper) { + public ResteasyValidationExceptionMapper(ResteasyViolationExceptionToErrorDtoMapper mapper) { this.mapper = mapper; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java new file mode 100644 index 0000000000..991aeedaeb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java @@ -0,0 +1,30 @@ +package sonia.scm.api.v2; + +import sonia.scm.ScmConstraintViolationException; +import sonia.scm.api.v2.resources.ScmViolationExceptionToErrorDtoMapper; + +import javax.inject.Inject; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class ScmConstraintValidationExceptionMapper implements ExceptionMapper { + + private final ScmViolationExceptionToErrorDtoMapper mapper; + + @Inject + public ScmConstraintValidationExceptionMapper(ScmViolationExceptionToErrorDtoMapper mapper) { + this.mapper = mapper; + } + + @Override + public Response toResponse(ScmConstraintViolationException exception) { + return Response + .status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(mapper.map(exception)) + .build(); + } +} 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 35f58cef90..e6cf6721a5 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,7 +39,8 @@ public class MapperModule extends AbstractModule { bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); - bind(ViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ViolationExceptionToErrorDtoMapper.class).getClass()); + bind(ResteasyViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ResteasyViolationExceptionToErrorDtoMapper.class).getClass()); + bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass()); bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass()); // no mapstruct required 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/ResteasyViolationExceptionToErrorDtoMapper.java similarity index 96% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResteasyViolationExceptionToErrorDtoMapper.java index e713b031f7..7bab2d4272 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ViolationExceptionToErrorDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResteasyViolationExceptionToErrorDtoMapper.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.stream.Collectors; @Mapper -public abstract class ViolationExceptionToErrorDtoMapper { +public abstract class ResteasyViolationExceptionToErrorDtoMapper { @Mapping(target = "errorCode", ignore = true) @Mapping(target = "transactionId", ignore = true) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java new file mode 100644 index 0000000000..3828134cc1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmViolationExceptionToErrorDtoMapper.java @@ -0,0 +1,53 @@ +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.ScmConstraintViolationException; +import sonia.scm.ScmConstraintViolationException.ScmConstraintViolation; + +import java.util.List; +import java.util.stream.Collectors; + +@Mapper +public abstract class ScmViolationExceptionToErrorDtoMapper { + + @Mapping(target = "errorCode", ignore = true) + @Mapping(target = "transactionId", ignore = true) + @Mapping(target = "context", ignore = true) + public abstract ErrorDto map(ScmConstraintViolationException exception); + + @AfterMapping + void setTransactionId(@MappingTarget ErrorDto dto) { + dto.setTransactionId(MDC.get("transaction_id")); + } + + @AfterMapping + void mapViolations(ScmConstraintViolationException exception, @MappingTarget ErrorDto dto) { + List violations = + exception.getViolations() + .stream() + .map(this::createViolationDto) + .collect(Collectors.toList()); + dto.setViolations(violations); + } + + private ErrorDto.ConstraintViolationDto createViolationDto(ScmConstraintViolation violation) { + ErrorDto.ConstraintViolationDto constraintViolationDto = new ErrorDto.ConstraintViolationDto(); + constraintViolationDto.setMessage(violation.getMessage()); + constraintViolationDto.setPath(violation.getPropertyPath()); + return constraintViolationDto; + } + + @AfterMapping + void setErrorCode(@MappingTarget ErrorDto dto) { + dto.setErrorCode("3zR9vPNIE1"); + } + + @AfterMapping + void setMessage(@MappingTarget ErrorDto dto) { + dto.setMessage("input violates conditions (see violation list)"); + } +} From 338d0657086d423ba850bf088607aa3fd557d9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Nov 2018 14:20:28 +0100 Subject: [PATCH 2/5] Fix serialization errors --- .../java/sonia/scm/ExceptionWithContext.java | 2 ++ .../java/sonia/scm/NotFoundException.java | 2 ++ .../scm/ScmConstraintViolationException.java | 28 +++++++++++-------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java index dd87b77210..8702567880 100644 --- a/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java +++ b/scm-core/src/main/java/sonia/scm/ExceptionWithContext.java @@ -6,6 +6,8 @@ import static java.util.Collections.unmodifiableList; public abstract class ExceptionWithContext extends RuntimeException { + private static final long serialVersionUID = 4327413456580409224L; + private final List context; public ExceptionWithContext(List context, String message) { diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 69b9617e93..9c478da855 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -7,6 +7,8 @@ import static java.util.stream.Collectors.joining; public class NotFoundException extends ExceptionWithContext { + private static final long serialVersionUID = 1710455380886499111L; + private static final String CODE = "AGR7UzkhA1"; public NotFoundException(Class type, String id) { diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java index 1fb31dc0fc..d50a1c7591 100644 --- a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -1,19 +1,22 @@ package sonia.scm; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import static java.util.Collections.unmodifiableCollection; -public class ScmConstraintViolationException extends RuntimeException { +public class ScmConstraintViolationException extends RuntimeException implements Serializable { + + private static final long serialVersionUID = 6904534307450229887L; private final Collection violations; - private final String furtherInformations; + private final String furtherInformation; - private ScmConstraintViolationException(Collection violations, String furtherInformations) { + private ScmConstraintViolationException(Collection violations, String furtherInformation) { this.violations = violations; - this.furtherInformations = furtherInformations; + this.furtherInformation = furtherInformation; } public Collection getViolations() { @@ -21,12 +24,12 @@ public class ScmConstraintViolationException extends RuntimeException { } public String getUrl() { - return furtherInformations; + return furtherInformation; } public static class Builder { private final Collection violations = new ArrayList<>(); - private String furtherInformations; + private String furtherInformation; public static Builder doThrow() { Builder builder = new Builder(); @@ -35,7 +38,7 @@ public class ScmConstraintViolationException extends RuntimeException { public Builder andThrow() { this.violations.clear(); - this.furtherInformations = null; + this.furtherInformation = null; return this; } @@ -44,19 +47,22 @@ public class ScmConstraintViolationException extends RuntimeException { return this; } - public Builder withFurtherInformations(String furtherInformations) { - this.furtherInformations = furtherInformations; + public Builder withFurtherInformation(String furtherInformation) { + this.furtherInformation = furtherInformation; return this; } public void when(boolean condition) { if (condition && !this.violations.isEmpty()) { - throw new ScmConstraintViolationException(violations, furtherInformations); + throw new ScmConstraintViolationException(violations, furtherInformation); } } } - public static class ScmConstraintViolation { + public static class ScmConstraintViolation implements Serializable { + + private static final long serialVersionUID = -6900317468157084538L; + private final String message; private final String path; From 82c8b86386ebf4a7723b2629768ac3d3c7f9f93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 27 Nov 2018 14:23:26 +0100 Subject: [PATCH 3/5] Fix sonar issue --- .../main/java/sonia/scm/ScmConstraintViolationException.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java index d50a1c7591..01c628e3dd 100644 --- a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -32,8 +32,7 @@ public class ScmConstraintViolationException extends RuntimeException implements private String furtherInformation; public static Builder doThrow() { - Builder builder = new Builder(); - return builder; + return new Builder(); } public Builder andThrow() { From b3dc39772e67f38f097209de7b3066fb2e54f379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 28 Nov 2018 10:41:21 +0100 Subject: [PATCH 4/5] Add JavaDoc --- .../scm/ScmConstraintViolationException.java | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java index 01c628e3dd..a28812eb8a 100644 --- a/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java +++ b/scm-core/src/main/java/sonia/scm/ScmConstraintViolationException.java @@ -6,6 +6,22 @@ import java.util.Collection; import static java.util.Collections.unmodifiableCollection; +/** + * Use this exception to handle invalid input values that cannot be handled using + * JEE bean validation. + * Use the {@link Builder} to conditionally create a new exception: + *
+ * Builder
+ *   .doThrow()
+ *   .violation("name or alias must not be empty if not anonymous", "myParameter", "name")
+ *   .violation("name or alias must not be empty if not anonymous", "myParameter", "alias")
+ *   .when(myParameter.getName() == null && myParameter.getAlias() == null && !myParameter.isAnonymous())
+ *   .andThrow()
+ *   .violation("name must be empty if anonymous", "myParameter", "name")
+ *   .when(myParameter.getName() != null && myParameter.isAnonymous());
+ * 
+ * Mind that using this way you do not have to use if-else constructs. + */ public class ScmConstraintViolationException extends RuntimeException implements Serializable { private static final long serialVersionUID = 6904534307450229887L; @@ -19,45 +35,84 @@ public class ScmConstraintViolationException extends RuntimeException implements this.furtherInformation = furtherInformation; } + /** + * The violations that caused this exception. + */ public Collection getViolations() { return unmodifiableCollection(violations); } + /** + * An optional URL for more informations about this constraint violation. + */ public String getUrl() { return furtherInformation; } + /** + * Builder to conditionally create constraint violations. + */ public static class Builder { private final Collection violations = new ArrayList<>(); private String furtherInformation; + /** + * Use this to create a new builder instance. + */ public static Builder doThrow() { return new Builder(); } + /** + * Resets this builder to check for further violations. + * @return this builder instance. + */ public Builder andThrow() { this.violations.clear(); this.furtherInformation = null; return this; } + /** + * Describes the violation with a custom message and the affected property. When more than one property is affected, + * you can call this method multiple times. + * @param message The message describing the violation. + * @param pathElements The affected property denoted by the path to reach this property, + * eg. "someParameter", "complexProperty", "attribute" + * @return this builder instance. + */ public Builder violation(String message, String... pathElements) { this.violations.add(new ScmConstraintViolation(message, pathElements)); return this; } + /** + * Use this to specify a URL with further information about this violation and hints how to solve this. + * This is optional. + * @return this builder instance. + */ public Builder withFurtherInformation(String furtherInformation) { this.furtherInformation = furtherInformation; return this; } - public void when(boolean condition) { + /** + * When the given condition is true, a exception will be thrown. Otherwise this simply resets this + * builder and does nothing else. + * @param condition The condition that indicates a violation of this constraint. + * @return this builder instance. + */ + public Builder when(boolean condition) { if (condition && !this.violations.isEmpty()) { throw new ScmConstraintViolationException(violations, furtherInformation); } + return andThrow(); } } + /** + * A single constraint violation. + */ public static class ScmConstraintViolation implements Serializable { private static final long serialVersionUID = -6900317468157084538L; From 58f63e79fa0430cd5fab1be11d7459a46b0e147b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 30 Nov 2018 08:37:42 +0000 Subject: [PATCH 5/5] Close branch feature/violations