diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml
index 965bddf2b0..6d3b94412d 100644
--- a/scm-webapp/pom.xml
+++ b/scm-webapp/pom.xml
@@ -99,6 +99,16 @@
jackson-datatype-jsr310
${jackson.version}
+
+ javax
+ javaee-api
+ 7.0
+
+
+ org.hibernate.validator
+ hibernate-validator
+ 6.0.12.Final
+
@@ -131,7 +141,11 @@
org.jboss.resteasy
resteasy-servlet-initializer
-
+
+ org.jboss.resteasy
+ resteasy-validator-provider-11
+ ${resteasy.version}
+
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
new file mode 100644
index 0000000000..53681bc9af
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationExceptionMapper.java
@@ -0,0 +1,58 @@
+package sonia.scm.api.v2;
+
+import lombok.Getter;
+import org.jboss.resteasy.api.validation.ResteasyViolationException;
+
+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.List;
+import java.util.stream.Collectors;
+
+@Provider
+public class ValidationExceptionMapper implements ExceptionMapper {
+
+ @Override
+ public Response toResponse(ResteasyViolationException exception) {
+
+ List violations =
+ exception.getConstraintViolations()
+ .stream()
+ .map(ConstraintViolationBean::new)
+ .collect(Collectors.toList());
+
+ return Response
+ .status(Response.Status.BAD_REQUEST)
+ .type(MediaType.APPLICATION_JSON_TYPE)
+ .entity(new ValidationError(violations))
+ .build();
+ }
+
+ @Getter
+ public static class ValidationError {
+ @XmlElement(name = "violation")
+ @XmlElementWrapper(name = "violations")
+ private List violoations;
+
+ public ValidationError(List violoations) {
+ this.violoations = violoations;
+ }
+ }
+
+ @XmlRootElement(name = "violation")
+ @Getter
+ public static class ConstraintViolationBean {
+ private String path;
+ private String message;
+
+ public ConstraintViolationBean(ConstraintViolation> violation) {
+ message = violation.getMessage();
+ path = violation.getPropertyPath().toString();
+ }
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java
index c6d54e7f4e..9fef539756 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java
@@ -1,13 +1,24 @@
package sonia.scm.api.v2.resources;
-import com.webcohesion.enunciate.metadata.rs.*;
+import com.webcohesion.enunciate.metadata.rs.ResponseCode;
+import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
+import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
+import com.webcohesion.enunciate.metadata.rs.StatusCodes;
+import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
-import javax.ws.rs.*;
+import javax.validation.Valid;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
public class RepositoryCollectionResource {
@@ -76,7 +87,7 @@ public class RepositoryCollectionResource {
})
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository"))
- public Response create(RepositoryDto repositoryDto) throws RepositoryException {
+ public Response create(@Valid RepositoryDto repositoryDto) throws RepositoryException {
return adapter.create(repositoryDto,
() -> dtoToRepositoryMapper.map(repositoryDto, null),
repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName()));
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java
index bcc8e16ebb..94fdb68cd6 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java
@@ -6,6 +6,7 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.Setter;
+import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.List;
import java.util.Map;
@@ -20,6 +21,7 @@ public class RepositoryDto extends HalRepresentation {
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
private String namespace;
+ @Pattern(regexp = "[\\w-]+", message = "The name must be a valid identifyer")
private String name;
private boolean archived = false;
private String type;