package uk.bl.api;
import javax.validation.*;
import javax.validation.metadata.*;
import java.util.*;
import java.lang.annotation.*;
import java.util.regex.Pattern;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import play.mvc.Http;
import static play.libs.F.*;
import play.libs.F.Tuple;
import play.data.DynamicForm;
import play.data.validation.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.*;
import org.springframework.validation.*;
import org.springframework.validation.beanvalidation.*;
import org.springframework.context.support.*;
/**
* Helper to manage HTML form description, submission and validation.
*/
public class UkwaForm<T> {
// -- Form utilities
/**
* Instantiates a dynamic form.
*/
public static DynamicForm form() {
return new DynamicForm();
}
/**
* Instantiates a new form that wraps the specified class.
*/
public static <T> UkwaForm<T> form(Class<T> clazz) {
return new UkwaForm<T>(clazz);
}
/**
* Instantiates a new form that wraps the specified class.
*/
public static <T> UkwaForm<T> form(String name, Class<T> clazz) {
return new UkwaForm<T>(name, clazz);
}
/**
* Instantiates a new form that wraps the specified class.
*/
public static <T> UkwaForm<T> form(String name, Class<T> clazz, Class<?> group) {
return new UkwaForm<T>(name, clazz, group);
}
/**
* Instantiates a new form that wraps the specified class.
*/
public static <T> UkwaForm<T> form(Class<T> clazz, Class<?> group) {
return new UkwaForm<T>(null, clazz, group);
}
// ---
/**
* Defines a form element's display name.
*/
@Retention(RUNTIME)
@Target({ ANNOTATION_TYPE })
public static @interface Display {
String name();
String[] attributes() default {};
}
// --
private final String rootName;
private final Class<T> backedType;
private final Map<String, String> data;
private final Map<String, List<ValidationError>> errors;
private final Option<T> value;
private final Class<?> groups;
private T blankInstance() {
try {
return backedType.newInstance();
} catch (Exception e) {
throw new RuntimeException("Cannot instantiate " + backedType
+ ". It must have a default constructor", e);
}
}
/**
* Creates a new <code>Form</code>.
*
* @param clazz
* wrapped class
*/
public UkwaForm(Class<T> clazz) {
this(null, clazz);
}
@SuppressWarnings("unchecked")
public UkwaForm(String name, Class<T> clazz) {
this(name, clazz, new HashMap<String, String>(),
new HashMap<String, List<ValidationError>>(), None(), null);
}
@SuppressWarnings("unchecked")
public UkwaForm(String name, Class<T> clazz, Class<?> groups) {
this(name, clazz, new HashMap<String, String>(),
new HashMap<String, List<ValidationError>>(), None(), groups);
}
public UkwaForm(String rootName, Class<T> clazz, Map<String, String> data,
Map<String, List<ValidationError>> errors, Option<T> value) {
this(rootName, clazz, data, errors, value, null);
}
/**
* Creates a new <code>Form</code>.
*
* @param clazz
* wrapped class
* @param data
* the current form data (used to display the form)
* @param errors
* the collection of errors associated with this form
* @param value
* optional concrete value of type <code>T</code> if the form
* submission was successful
*/
public UkwaForm(String rootName, Class<T> clazz, Map<String, String> data,
Map<String, List<ValidationError>> errors, Option<T> value,
Class<?> groups) {
this.rootName = rootName;
this.backedType = clazz;
this.data = data;
this.errors = errors;
this.value = value;
this.groups = groups;
}
protected Map<String, String> requestData(Http.Request request) {
Map<String, String[]> urlFormEncoded = new HashMap<String, String[]>();
if (request.body().asFormUrlEncoded() != null) {
urlFormEncoded = request.body().asFormUrlEncoded();
}
Map<String, String[]> multipartFormData = new HashMap<String, String[]>();
if (request.body().asMultipartFormData() != null) {
multipartFormData = request.body().asMultipartFormData()
.asFormUrlEncoded();
}
Map<String, String> jsonData = new HashMap<String, String>();
if (request.body().asJson() != null) {
jsonData = play.libs.Scala.asJava(play.api.data.FormUtils.fromJson(
"", play.api.libs.json.Json.parse(play.libs.Json
.stringify(request.body().asJson()))));
}
Map<String, String[]> queryString = request.queryString();
Map<String, String> data = new HashMap<String, String>();
for (String key : urlFormEncoded.keySet()) {
String[] values = urlFormEncoded.get(key);
if (key.endsWith("[]")) {
String k = key.substring(0, key.length() - 2);
for (int i = 0; i < values.length; i++) {
data.put(k + "[" + i + "]", values[i]);
}
} else {
if (values.length > 0) {
data.put(key, values[0]);
}
}
}
for (String key : multipartFormData.keySet()) {
String[] values = multipartFormData.get(key);
if (key.endsWith("[]")) {
String k = key.substring(0, key.length() - 2);
for (int i = 0; i < values.length; i++) {
data.put(k + "[" + i + "]", values[i]);
}
} else {
if (values.length > 0) {
data.put(key, values[0]);
}
}
}
for (String key : jsonData.keySet()) {
data.put(key, jsonData.get(key));
}
for (String key : queryString.keySet()) {
String[] values = queryString.get(key);
if (key.endsWith("[]")) {
String k = key.substring(0, key.length() - 2);
for (int i = 0; i < values.length; i++) {
data.put(k + "[" + i + "]", values[i]);
}
} else {
if (values.length > 0) {
data.put(key, values[0]);
}
}
}
return data;
}
/**
* Binds request data to this form - that is, handles form submission.
*
* @return a copy of this form filled with the new data
*/
public UkwaForm<T> bindFromRequest(String... allowedFields) {
return bind(requestData(play.mvc.Controller.request()), allowedFields);
}
/**
* Binds request data to this form - that is, handles form submission.
*
* @return a copy of this form filled with the new data
*/
public UkwaForm<T> bindFromRequest(Http.Request request,
String... allowedFields) {
return bind(requestData(request), allowedFields);
}
/**
* Binds request data to this form - that is, handles form submission.
*
* @return a copy of this form filled with the new data
*/
public UkwaForm<T> bindFromRequest(Map<String, String[]> requestData,
String... allowedFields) {
Map<String, String> data = new HashMap<String, String>();
for (String key : requestData.keySet()) {
String[] values = requestData.get(key);
if (key.endsWith("[]")) {
String k = key.substring(0, key.length() - 2);
for (int i = 0; i < values.length; i++) {
data.put(k + "[" + i + "]", values[i]);
}
} else {
if (values.length > 0) {
data.put(key, values[0]);
}
}
}
return bind(data, allowedFields);
}
/**
* Binds Json data to this form - that is, handles form submission.
*
* @param data
* data to submit
* @return a copy of this form filled with the new data
*/
public UkwaForm<T> bind(com.fasterxml.jackson.databind.JsonNode data,
String... allowedFields) {
return bind(
play.libs.Scala.asJava(play.api.data.FormUtils.fromJson("",
play.api.libs.json.Json.parse(play.libs.Json
.stringify(data)))), allowedFields);
}
private static final Set<String> internalAnnotationAttributes = new HashSet<String>(
3);
static {
internalAnnotationAttributes.add("message");
internalAnnotationAttributes.add("groups");
internalAnnotationAttributes.add("payload");
}
protected Object[] getArgumentsForConstraint(String objectName,
String field, ConstraintDescriptor<?> descriptor) {
List<Object> arguments = new LinkedList<Object>();
String[] codes = new String[] {
objectName + Errors.NESTED_PATH_SEPARATOR + field, field };
arguments.add(new DefaultMessageSourceResolvable(codes, field));
// Using a TreeMap for alphabetical ordering of attribute names
Map<String, Object> attributesToExpose = new TreeMap<String, Object>();
for (Map.Entry<String, Object> entry : descriptor.getAttributes()
.entrySet()) {
String attributeName = entry.getKey();
Object attributeValue = entry.getValue();
if (!internalAnnotationAttributes.contains(attributeName)) {
attributesToExpose.put(attributeName, attributeValue);
}
}
arguments.addAll(attributesToExpose.values());
return arguments.toArray(new Object[arguments.size()]);
}
/**
* When dealing with @ValidateWith annotations, and message parameter is not
* used in the annotation, extract the message from validator's
* getErrorMessageKey() method
**/
protected String getMessageForConstraintViolation(
ConstraintViolation<Object> violation) {
String errorMessage = violation.getMessage();
Annotation annotation = violation.getConstraintDescriptor()
.getAnnotation();
if (annotation instanceof Constraints.ValidateWith) {
Constraints.ValidateWith validateWithAnnotation = (Constraints.ValidateWith) annotation;
if (violation.getMessage().equals(
Constraints.ValidateWithValidator.message)) {
Constraints.ValidateWithValidator validateWithValidator = new Constraints.ValidateWithValidator();
validateWithValidator.initialize(validateWithAnnotation);
Tuple<String, Object[]> errorMessageKey = validateWithValidator
.getErrorMessageKey();
if (errorMessageKey != null && errorMessageKey._1 != null) {
errorMessage = errorMessageKey._1;
}
}
}
return errorMessage;
}
/**
* Binds data to this form - that is, handles form submission.
*
* @param data
* data to submit
* @return a copy of this form filled with the new data
*/
@SuppressWarnings("unchecked")
public UkwaForm<T> bind(Map<String, String> data, String... allowedFields) {
DataBinder dataBinder = null;
Map<String, String> objectData = data;
if (rootName == null) {
dataBinder = new DataBinder(blankInstance());
} else {
dataBinder = new DataBinder(blankInstance(), rootName);
objectData = new HashMap<String, String>();
for (String key : data.keySet()) {
if (key.startsWith(rootName + ".")) {
objectData.put(key.substring(rootName.length() + 1),
data.get(key));
}
}
}
if (allowedFields.length > 0) {
dataBinder.setAllowedFields(allowedFields);
}
SpringValidatorAdapter validator = new SpringValidatorAdapter(
play.data.validation.Validation.getValidator());
dataBinder.setValidator(validator);
dataBinder.setConversionService(play.data.format.Formatters.conversion);
dataBinder.setAutoGrowNestedPaths(true);
dataBinder.bind(new MutablePropertyValues(objectData));
Set<ConstraintViolation<Object>> validationErrors;
if (groups != null) {
validationErrors = validator.validate(dataBinder.getTarget(),
groups);
} else {
validationErrors = validator.validate(dataBinder.getTarget());
}
BindingResult result = dataBinder.getBindingResult();
for (ConstraintViolation<Object> violation : validationErrors) {
String field = violation.getPropertyPath().toString();
FieldError fieldError = result.getFieldError(field);
if (fieldError == null || !fieldError.isBindingFailure()) {
try {
result.rejectValue(
field,
violation.getConstraintDescriptor().getAnnotation()
.annotationType().getSimpleName(),
getArgumentsForConstraint(result.getObjectName(),
field, violation.getConstraintDescriptor()),
getMessageForConstraintViolation(violation));
} catch (NotReadablePropertyException ex) {
throw new IllegalStateException(
"JSR-303 validated property '"
+ field
+ "' does not have a corresponding accessor for data binding - "
+ "check your DataBinder's configuration (bean property versus direct field access)",
ex);
}
}
}
if (result.hasErrors() || result.getGlobalErrorCount() > 0) {
Map<String, List<ValidationError>> errors = new HashMap<String, List<ValidationError>>();
for (FieldError error : result.getFieldErrors()) {
String key = error.getObjectName() + "." + error.getField();
if (key.startsWith("target.") && rootName == null) {
key = key.substring(7);
}
if (!errors.containsKey(key)) {
errors.put(key, new ArrayList<ValidationError>());
}
ValidationError validationError = null;
if (error.isBindingFailure()) {
List<String> errorList = new ArrayList<String>();
// ImmutableList.Builder<String> builder = ImmutableList.builder();
for (String code : error.getCodes()) {
errorList.add(code.replace("typeMismatch",
"error.invalid"));
// builder.add(code.replace("typeMismatch",
// "error.invalid"));
}
String errorString = StringUtils.join(errorList, ", ");
validationError = new ValidationError(key, errorString, convertErrorArguments(error.getArguments()));
} else {
validationError = new ValidationError(key,
error.getDefaultMessage(),
convertErrorArguments(error.getArguments()));
}
errors.get(key).add(validationError);
}
List<ValidationError> globalErrors = new ArrayList<ValidationError>();
for (ObjectError error : result.getGlobalErrors()) {
globalErrors.add(new ValidationError("", error
.getDefaultMessage(), convertErrorArguments(error
.getArguments())));
}
if (!globalErrors.isEmpty()) {
errors.put("", globalErrors);
}
return new UkwaForm(rootName, backedType, data, errors, None(), groups);
} else {
Object globalError = null;
if (result.getTarget() != null) {
try {
java.lang.reflect.Method v = result.getTarget().getClass()
.getMethod("validate");
globalError = v.invoke(result.getTarget());
} catch (NoSuchMethodException e) {
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
if (globalError != null) {
Map<String, List<ValidationError>> errors = new HashMap<String, List<ValidationError>>();
if (globalError instanceof String) {
errors.put("", new ArrayList<ValidationError>());
errors.get("").add(
new ValidationError("", (String) globalError,
new ArrayList()));
} else if (globalError instanceof List) {
for (ValidationError error : (List<ValidationError>) globalError) {
List<ValidationError> errorsForKey = errors.get(error
.key());
if (errorsForKey == null) {
errors.put(
error.key(),
errorsForKey = new ArrayList<ValidationError>());
}
errorsForKey.add(error);
}
} else if (globalError instanceof Map) {
errors = (Map<String, List<ValidationError>>) globalError;
}
return new UkwaForm(rootName, backedType, data, errors, None(),
groups);
}
return new UkwaForm(rootName, backedType, new HashMap<String, String>(
data), new HashMap<String, List<ValidationError>>(errors),
Some((T) result.getTarget()), groups);
}
}
/**
* Convert the error arguments.
*
* @param arguments
* The arguments to convert.
* @return The converted arguments.
*/
private List<Object> convertErrorArguments(Object[] arguments) {
List<Object> converted = new ArrayList<Object>(arguments.length);
for (Object arg : arguments) {
if (!(arg instanceof org.springframework.context.support.DefaultMessageSourceResolvable)) {
converted.add(arg);
}
}
return Collections.unmodifiableList(converted);
}
/**
* Retrieves the actual form data.
*/
public Map<String, String> data() {
return data;
}
public String name() {
return rootName;
}
/**
* Retrieves the actual form value.
*/
public Option<T> value() {
return value;
}
/**
* Populates this form with an existing value, used for edit forms.
*
* @param value
* existing value of type <code>T</code> used to fill this form
* @return a copy of this form filled with the new data
*/
@SuppressWarnings("unchecked")
public UkwaForm<T> fill(T value) {
if (value == null) {
throw new RuntimeException("Cannot fill a form with a null value");
}
return new UkwaForm(rootName, backedType, new HashMap<String, String>(),
new HashMap<String, ValidationError>(), Some(value), groups);
}
/**
* Returns <code>true</code> if there are any errors related to this form.
*/
public boolean hasErrors() {
return !errors.isEmpty();
}
/**
* Returns <code>true</code> if there any global errors related to this
* form.
*/
public boolean hasGlobalErrors() {
return errors.containsKey("") && !errors.get("").isEmpty();
}
/**
* Retrieve all global errors - errors without a key.
*
* @return All global errors.
*/
public List<ValidationError> globalErrors() {
List<ValidationError> e = errors.get("");
if (e == null) {
e = new ArrayList<ValidationError>();
}
return e;
}
/**
* Retrieves the first global error (an error without any key), if it
* exists.
*
* @return An error or <code>null</code>.
*/
public ValidationError globalError() {
List<ValidationError> errors = globalErrors();
if (errors.isEmpty()) {
return null;
} else {
return errors.get(0);
}
}
/**
* Returns all errors.
*
* @return All errors associated with this form.
*/
public Map<String, List<ValidationError>> errors() {
return errors;
}
/**
* Retrieve an error by key.
*/
public ValidationError error(String key) {
List<ValidationError> err = errors.get(key);
if (err == null || err.isEmpty()) {
return null;
} else {
return err.get(0);
}
}
/**
* Returns the form errors serialized as Json.
*/
public com.fasterxml.jackson.databind.JsonNode errorsAsJson() {
return errorsAsJson(Http.Context.Implicit.lang());
}
/**
* Returns the form errors serialized as Json using the given Lang.
*/
public com.fasterxml.jackson.databind.JsonNode errorsAsJson(
play.i18n.Lang lang) {
Map<String, List<String>> allMessages = new HashMap<String, List<String>>();
for (String key : errors.keySet()) {
List<ValidationError> errs = errors.get(key);
if (errs != null && !errs.isEmpty()) {
List<String> messages = new ArrayList<String>();
for (ValidationError error : errs) {
messages.add(play.i18n.Messages.get(lang, error.message(),
error.arguments()));
}
allMessages.put(key, messages);
}
}
return play.libs.Json.toJson(allMessages);
}
/**
* Gets the concrete value if the submission was a success.
*
* @throws IllegalStateException
* if there are errors binding the form, including the errors as
* JSON in the message
*/
public T get() {
// if (!errors.isEmpty()) {
// throw new IllegalStateException("Error(s) binding form: "
// + errorsAsJson());
// }
System.out.println("THE VALUE: " + value);
return value.get();
}
/**
* Adds an error to this form.
*
* @param error
* the <code>ValidationError</code> to add.
*/
public void reject(ValidationError error) {
if (!errors.containsKey(error.key())) {
errors.put(error.key(), new ArrayList<ValidationError>());
}
errors.get(error.key()).add(error);
}
/**
* Adds an error to this form.
*
* @param key
* the error key
* @param error
* the error message
* @param args
* the error arguments
*/
public void reject(String key, String error, List<Object> args) {
reject(new ValidationError(key, error, args));
}
/**
* Adds an error to this form.
*
* @param key
* the error key
* @param error
* the error message
*/
public void reject(String key, String error) {
reject(key, error, new ArrayList<Object>());
}
/**
* Adds a global error to this form.
*
* @param error
* the error message
* @param args
* the errot arguments
*/
public void reject(String error, List<Object> args) {
reject(new ValidationError("", error, args));
}
/**
* Add a global error to this form.
*
* @param error
* the error message.
*/
public void reject(String error) {
reject("", error, new ArrayList<Object>());
}
/**
* Discard errors of this form
*/
public void discardErrors() {
errors.clear();
}
/**
* Retrieve a field.
*
* @param key
* field name
* @return the field (even if the field does not exist you get a field)
*/
public Field apply(String key) {
return field(key);
}
/**
* Retrieve a field.
*
* @param key
* field name
* @return the field (even if the field does not exist you get a field)
*/
public Field field(String key) {
// Value
String fieldValue = null;
if (data.containsKey(key)) {
fieldValue = data.get(key);
} else {
if (value.isDefined()) {
BeanWrapper beanWrapper = new BeanWrapperImpl(value.get());
beanWrapper.setAutoGrowNestedPaths(true);
String objectKey = key;
if (rootName != null && key.startsWith(rootName + ".")) {
objectKey = key.substring(rootName.length() + 1);
}
if (beanWrapper.isReadableProperty(objectKey)) {
Object oValue = beanWrapper.getPropertyValue(objectKey);
if (oValue != null) {
fieldValue = play.data.format.Formatters.print(
beanWrapper
.getPropertyTypeDescriptor(objectKey),
oValue);
}
}
}
}
// Error
List<ValidationError> fieldErrors = errors.get(key);
if (fieldErrors == null) {
fieldErrors = new ArrayList<ValidationError>();
}
// Format
Tuple<String, List<Object>> format = null;
BeanWrapper beanWrapper = new BeanWrapperImpl(blankInstance());
beanWrapper.setAutoGrowNestedPaths(true);
try {
for (Annotation a : beanWrapper.getPropertyTypeDescriptor(key)
.getAnnotations()) {
Class<?> annotationType = a.annotationType();
if (annotationType
.isAnnotationPresent(play.data.Form.Display.class)) {
play.data.Form.Display d = annotationType
.getAnnotation(play.data.Form.Display.class);
if (d.name().startsWith("format.")) {
List<Object> attributes = new ArrayList<Object>();
for (String attr : d.attributes()) {
Object attrValue = null;
try {
attrValue = a.getClass()
.getDeclaredMethod(attr).invoke(a);
} catch (Exception e) {
}
attributes.add(attrValue);
}
format = Tuple(d.name(), attributes);
}
}
}
} catch (NullPointerException e) {
}
// Constraints
List<Tuple<String, List<Object>>> constraints = new ArrayList<Tuple<String, List<Object>>>();
Class<?> classType = backedType;
String leafKey = key;
if (rootName != null && leafKey.startsWith(rootName + ".")) {
leafKey = leafKey.substring(rootName.length() + 1);
}
int p = leafKey.lastIndexOf('.');
if (p > 0) {
classType = beanWrapper.getPropertyType(leafKey.substring(0, p));
leafKey = leafKey.substring(p + 1);
}
if (classType != null) {
BeanDescriptor beanDescriptor = play.data.validation.Validation
.getValidator().getConstraintsForClass(classType);
if (beanDescriptor != null) {
PropertyDescriptor property = beanDescriptor
.getConstraintsForProperty(leafKey);
if (property != null) {
constraints = Constraints.displayableConstraint(property
.getConstraintDescriptors());
}
}
}
return new Field(this, key, constraints, format, fieldErrors,
fieldValue);
}
@Override
public String toString() {
return "Form(of=" + backedType + ", data=" + data + ", value=" + value
+ ", errors=" + errors + ")";
}
/**
* A form field.
*/
public static class Field {
private final UkwaForm<?> form;
private final String name;
private final List<Tuple<String, List<Object>>> constraints;
private final Tuple<String, List<Object>> format;
private final List<ValidationError> errors;
private final String value;
/**
* Creates a form field.
*
* @param name
* the field name
* @param constraints
* the constraints associated with the field
* @param format
* the format expected for this field
* @param errors
* the errors associated with this field
* @param value
* the field value ,if any
*/
public Field(UkwaForm<?> form, String name,
List<Tuple<String, List<Object>>> constraints,
Tuple<String, List<Object>> format,
List<ValidationError> errors, String value) {
this.form = form;
this.name = name;
this.constraints = constraints;
this.format = format;
this.errors = errors;
this.value = value;
}
/**
* Returns the field name.
*
* @return The field name.
*/
public String name() {
return name;
}
/**
* Returns the field value, if defined.
*
* @return The field value, if defined.
*/
public String value() {
return value;
}
public String valueOr(String or) {
if (value == null) {
return or;
}
return value;
}
/**
* Returns all the errors associated with this field.
*
* @return The errors associated with this field.
*/
public List<ValidationError> errors() {
return errors;
}
/**
* Returns all the constraints associated with this field.
*
* @return The constraints associated with this field.
*/
public List<Tuple<String, List<Object>>> constraints() {
return constraints;
}
/**
* Returns the expected format for this field.
*
* @return The expected format for this field.
*/
public Tuple<String, List<Object>> format() {
return format;
}
/**
* Return the indexes available for this field (for repeated fields ad
* List)
*/
@SuppressWarnings("rawtypes")
public List<Integer> indexes() {
if (form.value().isDefined()) {
BeanWrapper beanWrapper = new BeanWrapperImpl(form.value()
.get());
beanWrapper.setAutoGrowNestedPaths(true);
String objectKey = name;
if (form.name() != null && name.startsWith(form.name() + ".")) {
objectKey = name.substring(form.name().length() + 1);
}
List<Integer> result = new ArrayList<Integer>();
if (beanWrapper.isReadableProperty(objectKey)) {
Object value = beanWrapper.getPropertyValue(objectKey);
if (value instanceof Collection) {
for (int i = 0; i < ((Collection) value).size(); i++) {
result.add(i);
}
}
}
return result;
} else {
Set<Integer> result = new HashSet<Integer>();
Pattern pattern = Pattern.compile("^" + Pattern.quote(name)
+ "\\[(\\d+)\\].*$");
for (String key : form.data().keySet()) {
java.util.regex.Matcher matcher = pattern.matcher(key);
if (matcher.matches()) {
result.add(Integer.parseInt(matcher.group(1)));
}
}
List<Integer> sortedResult = new ArrayList<Integer>(result);
Collections.sort(sortedResult);
return sortedResult;
}
}
/**
* Get a sub-field, with a key relative to the current field.
*/
public Field sub(String key) {
String subKey = null;
if (key.startsWith("[")) {
subKey = name + key;
} else {
subKey = name + "." + key;
}
return form.field(subKey);
}
@Override
public String toString() {
return "Form.Field(" + name + ")";
}
}
}