Skip to content

Commit

Permalink
Merge branch 'validations-enhancements' into master, fixing #137
Browse files Browse the repository at this point in the history
  • Loading branch information
fcamblor committed Jan 22, 2015
2 parents 3a25964 + 6bff1cc commit a516b4e
Show file tree
Hide file tree
Showing 25 changed files with 500 additions and 37 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -47,7 +47,7 @@ Here is a brief summary of each module:
- `restx-factory`: RESTX Dependency Injection (DI) container. Brought as transitive dependency from restx-core. - `restx-factory`: RESTX Dependency Injection (DI) container. Brought as transitive dependency from restx-core.
- `restx-classloader`: Hot reload / hot compile support - `restx-classloader`: Hot reload / hot compile support
- `restx-apidocs-doclet`: Some javadoc doclets used when generating apidocs - `restx-apidocs-doclet`: Some javadoc doclets used when generating apidocs
- `restx-core`: Core module, includes the REST framework, base security, JSON support, Validation, ... - `restx-core`: Core module, includes the REST framework, base security, JSON support, ...


*By relying on `restx-core` module, every modules described above will be retrieved as transitive dependencies* *By relying on `restx-core` module, every modules described above will be retrieved as transitive dependencies*


Expand All @@ -63,6 +63,7 @@ Here is a brief summary of each module:
- `restx-specs-tests-java8`: Support for java 8 time API during specs tests - `restx-specs-tests-java8`: Support for java 8 time API during specs tests
- `restx-specs-server`: Enables using RESTX specs as HTTP mocks (running a server serving spec files responses given spec files requests). - `restx-specs-server`: Enables using RESTX specs as HTTP mocks (running a server serving spec files responses given spec files requests).
- `restx-factory-testing`: A module dedicated to test `restx-factory` features involving annotation processing. - `restx-factory-testing`: A module dedicated to test `restx-factory` features involving annotation processing.
- `restx-validation`: Bean validation support (based on `hibernate-validator` implementation) for POJOs BODY parameters




#### MongoDB support through Jongo API: #### MongoDB support through Jongo API:
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Expand Up @@ -158,6 +158,7 @@
<module>restx-shell-manager</module> <module>restx-shell-manager</module>
<module>restx-build-shell</module> <module>restx-build-shell</module>
<module>restx-specs-shell</module> <module>restx-specs-shell</module>
<module>restx-validation</module>
</modules> </modules>




Expand Down
@@ -0,0 +1,38 @@
package restx.annotations;

import com.sun.tools.javac.code.Attribute;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
* @author fcamblor
*/
public class Annotations {
public static String[] getAnnotationClassValuesAsFQCN(VariableElement p, Class annotationClazz, String methodName) {
List<? extends AnnotationMirror> annotationMirrors = p.getAnnotationMirrors();
for(AnnotationMirror annotationMirror : annotationMirrors){
if(annotationMirror.getAnnotationType().toString().equals(annotationClazz.getCanonicalName())){
for(Map.Entry<? extends ExecutableElement,? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()){
if(entry.getKey().getSimpleName().contentEquals(methodName)){
Attribute.Array array = (Attribute.Array) entry.getValue();

List<String> fqcns = new ArrayList<>();
for(Attribute attribute : array.getValue()) {
DeclaredType type = (DeclaredType) attribute.getValue();
fqcns.add(type.toString());
}
return fqcns.toArray(new String[fqcns.size()]);
}
}
}
}
return null;
}
}
@@ -1,6 +1,7 @@
package restx.annotations.processor; package restx.annotations.processor;


import com.google.common.base.CaseFormat; import com.google.common.base.CaseFormat;
import com.google.common.base.Function;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.collect.*; import com.google.common.collect.*;
Expand All @@ -14,6 +15,7 @@
import restx.http.HttpStatus; import restx.http.HttpStatus;
import restx.security.PermitAll; import restx.security.PermitAll;
import restx.security.RolesAllowed; import restx.security.RolesAllowed;
import restx.validation.ValidatedFor;


import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedAnnotationTypes;
Expand All @@ -38,6 +40,13 @@
public class RestxAnnotationProcessor extends RestxAbstractProcessor { public class RestxAnnotationProcessor extends RestxAbstractProcessor {
final Template routerTpl; final Template routerTpl;


private static final Function<Class,String> FQN_EXTRACTOR = new Function<Class,String>(){
@Override
public String apply(Class clazz) {
return clazz.getCanonicalName();
}
};

public RestxAnnotationProcessor() { public RestxAnnotationProcessor() {
routerTpl = Mustaches.compile(RestxAnnotationProcessor.class, "RestxRouter.mustache"); routerTpl = Mustaches.compile(RestxAnnotationProcessor.class, "RestxRouter.mustache");
} }
Expand Down Expand Up @@ -163,11 +172,19 @@ private void buildResourceMethodParams(ResourceMethodAnnotation annotation, Reso
} }
} }


ValidatedFor validatedFor = p.getAnnotation(ValidatedFor.class);

String[] validationGroups = new String[0];
if(validatedFor != null) {
validationGroups = Annotations.getAnnotationClassValuesAsFQCN(p, ValidatedFor.class, "value");
}

resourceMethod.parameters.add(new ResourceMethodParameter( resourceMethod.parameters.add(new ResourceMethodParameter(
p.asType().toString(), p.asType().toString(),
paramName, paramName,
reqParamName, reqParamName,
parameterKind)); parameterKind,
validationGroups));
} }
if (!pathParamNamesToMatch.isEmpty()) { if (!pathParamNamesToMatch.isEmpty()) {
error( error(
Expand Down Expand Up @@ -492,8 +509,16 @@ private static class ResourceMethodParameter {
final String name; final String name;
final String reqParamName; final String reqParamName;
final ResourceMethodParameterKind kind; final ResourceMethodParameterKind kind;
final List<String> validationGroupsFQNs;

private static final Function<String, String> CLASS_APPENDER_FCT = new Function<String, String>() {
@Override
public String apply(String fqn) {
return fqn+".class";
}
};


private ResourceMethodParameter(String type, String name, String reqParamName, ResourceMethodParameterKind kind) { private ResourceMethodParameter(String type, String name, String reqParamName, ResourceMethodParameterKind kind, String[] validationGroupsFQNs) {
Matcher guavaOptionalMatcher = guavaOptionalPattern.matcher(type); Matcher guavaOptionalMatcher = guavaOptionalPattern.matcher(type);
Matcher java8OptionalMatcher = java8OptionalPattern.matcher(type); Matcher java8OptionalMatcher = java8OptionalPattern.matcher(type);
this.realType = type; this.realType = type;
Expand All @@ -513,6 +538,15 @@ private ResourceMethodParameter(String type, String name, String reqParamName, R
this.name = name; this.name = name;
this.reqParamName = reqParamName; this.reqParamName = reqParamName;
this.kind = kind; this.kind = kind;
this.validationGroupsFQNs = Arrays.asList(validationGroupsFQNs);
}

public Optional<String> joinedValidationGroupFQNExpression(){
if(this.validationGroupsFQNs == null || this.validationGroupsFQNs.isEmpty()) {
return Optional.absent();
} else {
return Optional.of(Joiner.on(", ").join(Iterables.transform(this.validationGroupsFQNs, CLASS_APPENDER_FCT)));
}
} }
} }


Expand All @@ -539,7 +573,8 @@ public String fetchFromReqCode(ResourceMethodParameter parameter) {
}, },
BODY { BODY {
public String fetchFromReqCode(ResourceMethodParameter parameter) { public String fetchFromReqCode(ResourceMethodParameter parameter) {
return String.format("checkValid(validator, body)", parameter.type); Optional<String> validationGroupsExpr = parameter.joinedValidationGroupFQNExpression();
return String.format("checkValid(validator, body%s)", validationGroupsExpr.isPresent()?","+validationGroupsExpr.get():"");
} }
}, },
CONTEXT { CONTEXT {
Expand Down
Expand Up @@ -32,7 +32,7 @@ public class {{router}} extends RestxRouter {
final EntityRequestBodyReaderRegistry readerRegistry, final EntityRequestBodyReaderRegistry readerRegistry,
final EntityResponseWriterRegistry writerRegistry, final EntityResponseWriterRegistry writerRegistry,
final MainStringConverter converter, final MainStringConverter converter,
final Validator validator, final Optional<Validator> validator,
final RestxSecurityManager securityManager) { final RestxSecurityManager securityManager) {
super( super(
"{{routerGroup}}", "{{router}}", new RestxRoute[] { "{{routerGroup}}", "{{router}}", new RestxRoute[] {
Expand Down
8 changes: 4 additions & 4 deletions restx-core/pom.xml
Expand Up @@ -25,7 +25,7 @@
<joda-time.version>2.3</joda-time.version> <joda-time.version>2.3</joda-time.version>
<jackson.version>2.3.3</jackson.version> <jackson.version>2.3.3</jackson.version>
<snakeyaml.version>1.13</snakeyaml.version> <snakeyaml.version>1.13</snakeyaml.version>
<hibernate-validator.version>5.0.1.Final</hibernate-validator.version> <validation-api.version>1.1.0.Final</validation-api.version>
<reflections.version>0.9.9-RC1</reflections.version> <reflections.version>0.9.9-RC1</reflections.version>
<javax.inject.version>1</javax.inject.version> <javax.inject.version>1</javax.inject.version>
<junit.version>4.11</junit.version> <junit.version>4.11</junit.version>
Expand Down Expand Up @@ -94,9 +94,9 @@
<version>${slf4j-api.version}</version> <version>${slf4j-api.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.hibernate</groupId> <groupId>javax.validation</groupId>
<artifactId>hibernate-validator</artifactId> <artifactId>validation-api</artifactId>
<version>${hibernate-validator.version}</version> <version>${validation-api.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.reflections</groupId> <groupId>org.reflections</groupId>
Expand Down
13 changes: 13 additions & 0 deletions restx-core/src/main/java/restx/validation/ValidatedFor.java
@@ -0,0 +1,13 @@
package restx.validation;

import java.lang.annotation.*;

/**
* @author fcamblor
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidatedFor {
Class[] value();
}
24 changes: 21 additions & 3 deletions restx-core/src/main/java/restx/validation/Validations.java
@@ -1,9 +1,11 @@
package restx.validation; package restx.validation;


import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Optional;


import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolation;
import javax.validation.Validator; import javax.validation.Validator;
import javax.validation.groups.Default;
import java.util.Set; import java.util.Set;


/** /**
Expand All @@ -12,11 +14,27 @@
* Time: 9:57 PM * Time: 9:57 PM
*/ */
public class Validations { public class Validations {

/**
* @deprecated Kept for backward compat. Use checkValid(Optional&lt;Validator&gt;, T, Class...) instead
*/
@Deprecated
public static <T> T checkValid(Validator validator, T o) { public static <T> T checkValid(Validator validator, T o) {
Set<ConstraintViolation<T>> violations = validator.validate(o); return checkValid(Optional.of(validator), o);
if (!violations.isEmpty()) { }
throw new IllegalArgumentException(Joiner.on(", ").join(violations));
public static <T> T checkValid(Optional<Validator> validator, T o, Class... groups) {
if(validator.isPresent()) {
if(groups == null || groups.length==0) {
groups = new Class[]{ Default.class };
}

Set<ConstraintViolation<T>> violations = validator.get().validate(o, groups);
if (!violations.isEmpty()) {
throw new IllegalArgumentException(Joiner.on(", ").join(violations));
}
} }

return o; return o;
} }
} }
25 changes: 0 additions & 25 deletions restx-core/src/main/java/restx/validation/ValidatorFactory.java

This file was deleted.

@@ -0,0 +1,14 @@
package restx.validation.stereotypes;

import javax.validation.groups.Default;

/**
* @author fcamblor
*/
public interface FormValidations {
public static final String DefaultFQN = "javax.validation.groups.Default";
public static final String CreateFQN = "restx.validation.stereotypes.FormValidations.Create";
public static interface Create extends Default{}
public static final String UpdateFQN = "restx.validation.stereotypes.FormValidations.Update";
public static interface Update extends Default{}
}
19 changes: 19 additions & 0 deletions restx-samplest/pom.xml
Expand Up @@ -30,6 +30,11 @@
<artifactId>restx-core</artifactId> <artifactId>restx-core</artifactId>
<version>${restx.version}</version> <version>${restx.version}</version>
</dependency> </dependency>
<dependency>
<groupId>io.restx</groupId>
<artifactId>restx-validation</artifactId>
<version>${restx.version}</version>
</dependency>
<dependency> <dependency>
<groupId>io.restx</groupId> <groupId>io.restx</groupId>
<artifactId>restx-core-annotation-processor</artifactId> <artifactId>restx-core-annotation-processor</artifactId>
Expand Down Expand Up @@ -123,6 +128,20 @@
<additionalparam>-restx-target-dir ${project.basedir}/target/classes</additionalparam> <additionalparam>-restx-target-dir ${project.basedir}/target/classes</additionalparam>
</configuration> </configuration>
</plugin> </plugin>
<!-- Uncomment to debug annotation processing on samplest...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<verbose>true</verbose>
<fork>true</fork>
<compilerArgs>
<arg>-J-Xdebug</arg>
<arg>-J-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000</arg>
</compilerArgs>
</configuration>
</plugin>
-->
</plugins> </plugins>
</build> </build>
</project> </project>
52 changes: 52 additions & 0 deletions restx-samplest/src/main/java/samplest/validation/POJO.java
@@ -0,0 +1,52 @@
package samplest.validation;

import org.hibernate.validator.constraints.Email;
import restx.validation.stereotypes.FormValidations;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class POJO {
@NotNull(groups={FormValidations.Update.class})
Long id;
@NotNull
@Size(min=10, groups={ValidationResource.MyCustomValidationGroup.class})
String name;
@Valid
SubPOJO subPOJO;
@Email
String email;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public SubPOJO getSubPOJO() {
return subPOJO;
}

public void setSubPOJO(SubPOJO subPOJO) {
this.subPOJO = subPOJO;
}
}
16 changes: 16 additions & 0 deletions restx-samplest/src/main/java/samplest/validation/SubPOJO.java
@@ -0,0 +1,16 @@
package samplest.validation;

import javax.validation.constraints.Size;

public class SubPOJO {
@Size(min=10)
String label;

public String getLabel() {
return label;
}

public void setLabel(String label) {
this.label = label;
}
}

0 comments on commit a516b4e

Please sign in to comment.