Last active
August 29, 2015 13:57
-
-
Save sscovil/9815169 to your computer and use it in GitHub Desktop.
Java Object Validation via Annotation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Use this annotation to mark properties of a class as required in one or more validation contexts (e.g. CREATE, | |
* UPDATE), then validate instances of that class using the static Validator.validate(object, context) method. | |
*/ | |
@Target(ElementType.FIELD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface Required { | |
/** | |
* ValidationContext(s) in which a property is required. | |
*/ | |
ValidationContext[] context(); | |
/** | |
* Optional custom validation error message. | |
*/ | |
String message() default "This field is required."; | |
/** | |
* Set to true if this property is an object that should be validated recursively. | |
*/ | |
boolean introspect() default false; | |
/** | |
* Set to true if this property is not required, but should be validated recursively when set. Use with introspect. | |
*/ | |
boolean nullable() default false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum ValidationContext { | |
CREATE, | |
UPDATE | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.Map; | |
public class ValidationException extends RuntimeException { | |
private Map<String, String> errorCollection; | |
public ValidationException(String message) { | |
super(message); | |
} | |
public ValidationException(String message, Exception e) { | |
super(message, e); | |
} | |
public ValidationException(Exception e) { | |
super(e); | |
} | |
public ValidationException(Map<String, String> errorCollection) { | |
super(); | |
this.errorCollection = errorCollection; | |
} | |
public Map<String, String> getErrorCollection() { | |
return errorCollection; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.Map; | |
public interface ValidationFilter { | |
public void execute(Map<String, String> errorMap, ValidationContext context); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.lang.reflect.Field; | |
import java.util.HashMap; | |
import java.util.Map; | |
public class Validator { | |
/** | |
* Validate | |
* | |
* @param validationObject Object to validate. | |
* @param context ValidationContext to compare with @Required annotations for validationObject fields. | |
* @throws ValidationException | |
*/ | |
public static void validate(Object validationObject, ValidationContext context) throws ValidationException { | |
validate(validationObject, context, null); | |
} | |
/** | |
* Validate w/Filter | |
* | |
* @param validationObject Object to validate. | |
* @param context ValidationContext to compare with @Required annotations for validationObject fields. | |
* @param filter Optional ValidationFilter used to filter the errorMap before an exception is thrown. | |
* @throws ValidationException | |
*/ | |
public static void validate( | |
Object validationObject, | |
ValidationContext context, | |
ValidationFilter filter | |
) throws ValidationException { | |
Map<String, String> errorMap = executeValidation(validationObject, context, null); | |
if (filter != null) | |
filter.execute(errorMap, context); | |
if(!errorMap.isEmpty()) | |
throw new ValidationException(errorMap); | |
} | |
/** | |
* Execute Validation | |
* | |
* @param validationObject Object to validate. | |
* @param context ValidationContext to compare with @Required annotations for validationObject fields. | |
* @return ErrorMap containing missing required field names as keys and their @Required messages as values. | |
*/ | |
private static Map<String, String> executeValidation( | |
Object validationObject, | |
ValidationContext context, | |
String parentName | |
) { | |
Map<String, String> errorMap = new HashMap<String, String>(); | |
Field[] fields = validationObject.getClass().getDeclaredFields(); | |
for (Field field : fields) { | |
if (field.isAnnotationPresent(Required.class)) { | |
Required required = field.getAnnotation(Required.class); | |
if (requiredInContext(required.context(), context)) { | |
Object value = getFieldValue(validationObject, field.getName()); | |
if ((value == null || value.toString().isEmpty()) && !required.nullable()) { | |
if (parentName == null) parentName = ""; | |
else if (!parentName.isEmpty()) parentName += "."; | |
errorMap.put(parentName + field.getName(), required.message()); | |
} | |
else if (value != null && required.introspect()) { | |
// Use recursion to perform introspective validation on property. | |
errorMap.putAll(executeValidation(value, context, field.getName())); | |
} | |
} | |
} | |
} | |
return errorMap; | |
} | |
/** | |
* Required In Context | |
* | |
* @param requiredContexts Array of required ValidationContexts. | |
* @param currentContext ValidationContext to search for in requiredContexts. | |
* @return True if currentContext is present in requiredContexts; otherwise false. | |
*/ | |
private static boolean requiredInContext(ValidationContext[] requiredContexts, ValidationContext currentContext) { | |
if (requiredContexts != null && requiredContexts.length > 0) | |
for (ValidationContext requiredContext : requiredContexts) | |
if (requiredContext.equals(currentContext)) | |
return true; | |
return false; | |
} | |
/** | |
* Get Field Value | |
* | |
* @param object Object to get the field value from. | |
* @param fieldName Name of field to get the value of. | |
* @return Value of the specified object field. | |
*/ | |
private static Object getFieldValue(Object object, String fieldName) { | |
try { | |
Field field = object.getClass().getDeclaredField(fieldName); | |
field.setAccessible(true); | |
return field.get(object); | |
} | |
catch (NullPointerException e) { | |
return null; | |
} | |
catch (NoSuchFieldException e) { | |
return null; | |
} | |
catch (IllegalAccessException e) { | |
return null; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.mediasilo.api.validation.ValidationContext; | |
import com.mediasilo.api.validation.ValidationException; | |
import com.mediasilo.api.validation.Validator; | |
import org.testng.annotations.Test; | |
public class ValidatorTest { | |
@Test | |
public void createValidatorPasses() { | |
ValidatorTestObject object = new ValidatorTestObject(null, "required", "required"); | |
Validator.validate(object, ValidationContext.CREATE); | |
} | |
@Test(expectedExceptions = ValidationException.class) | |
public void createValidatorFails() { | |
ValidatorTestObject object = new ValidatorTestObject(); | |
Validator.validate(object, ValidationContext.CREATE); | |
} | |
@Test | |
public void updateValidatorPasses() { | |
ValidatorTestObject object = new ValidatorTestObject(12345, null, "required"); | |
Validator.validate(object, ValidationContext.UPDATE); | |
} | |
@Test(expectedExceptions = ValidationException.class) | |
public void updateValidatorFails() { | |
ValidatorTestObject object = new ValidatorTestObject(); | |
Validator.validate(object, ValidationContext.UPDATE); | |
} | |
@Test | |
public void requireIntrospectPasses() { | |
ValidatorTestObjectProperty introspect = new ValidatorTestObjectProperty("required"); | |
ValidatorTestObject object = new ValidatorTestObject(null, "required", "required", introspect); | |
Validator.validate(object, ValidationContext.CREATE); | |
} | |
@Test(expectedExceptions = ValidationException.class) | |
public void requireIntrospectFails() { | |
ValidatorTestObjectProperty introspect = new ValidatorTestObjectProperty(); | |
ValidatorTestObject object = new ValidatorTestObject(null, "required", "required", introspect); | |
Validator.validate(object, ValidationContext.CREATE); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.mediasilo.api.validation.Required; | |
import com.mediasilo.api.validation.ValidationContext; | |
public class ValidatorTestObject { | |
@Required(context = ValidationContext.UPDATE, message = "This field is required on update.") | |
private Integer id; | |
@Required(context = ValidationContext.CREATE, message = "This field is required on create.") | |
private String foo; | |
@Required(context = {ValidationContext.CREATE, ValidationContext.UPDATE}, | |
message = "This field is required on create and update.") | |
private String bar; | |
@Required(context = {ValidationContext.CREATE, ValidationContext.UPDATE}, introspect = true, nullable = true) | |
private ValidatorTestObjectProperty inner; | |
public ValidatorTestObject() { | |
} | |
public ValidatorTestObject(Integer id, String foo, String bar) { | |
this.id = id; | |
this.foo = foo; | |
this.bar = bar; | |
} | |
public ValidatorTestObject(Integer id, String foo, String bar, ValidatorTestObjectProperty inner) { | |
this.id = id; | |
this.foo = foo; | |
this.bar = bar; | |
this.inner = inner; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.mediasilo.api.validation.Required; | |
import com.mediasilo.api.validation.ValidationContext; | |
public class ValidatorTestObjectProperty { | |
@Required(context = {ValidationContext.CREATE, ValidationContext.UPDATE}) | |
private String required; | |
private String notRequired; | |
public ValidatorTestObjectProperty() { | |
} | |
public ValidatorTestObjectProperty(String required) { | |
this.required = required; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment