I try to write checks for the play-framework and see two different possibilities. I described both and want to know if my understanding is correct (So it's more a tutorial than a question, specially because I didn't get any response that I missed something). So what possibilities exists.
- The simple way: Extending the class
Check
:
Advantages: Easier to write, easier to read
Disadvantages: You can't parametrized the check, you can only define the message. - The advanced way: Writing an check based on OVal
AbstractAnnotationCheck
.
Advantages: You can parametrized the check and have a simpler to use annotation
Disadvantages: A little bit more complicated.
Before we have a look on the implementation I want to explain the messages. You can always set the message directly or use a key to refer the message in a message-properties. The last one is the cleaner and recommended way. Every validation get a least 1 parameter: The name of the property which isn't valid. So validation or check specific parameters are always referred with %i$s
where i>1. The format of the message string should follows the rules of Formatter but I'm unsure if all features are supported. As far as I know only %s, %d and %f is supported togeter with positioning. So %[argument_index$][flags]conversion
where conversion could only be s,d or f.
Lets have a look on two examples: The simple way I used in my module for optimistic locking:
/**
* Check with proof if the version of the current edited object is lesser
* than the version in db.
* Messagecode: optimisticLocking.modelHasChanged
* Parameter: 1 the request URL.
* Example-Message: The object was changed. <a href="%2$s">Reload</a> and do your changes again.
*
*/
static class OptimisticLockingCheck extends Check {
/**
* {@inheritDoc}
*/
@Override
public boolean isSatisfied(Object model, Object optimisiticLockingViolatedValue) {
//The comparision of version was made in the setter. Here
//we only have to check the flag.
if (((VersionedModel) model).optimisiticLockingViolated) {
final Request request = Request.current();
//The following doesn't work in 1.0 but in 1.1 see https://bugs.launchpad.net/play/+bug/634719
//http://play.lighthouseapp.com/projects/57987-play-framework/tickets/116
//setMessage(checkWithCheck.getMessage(), request != null ? request.url : "");
setMessage("optimisticLocking.modelHasChanged", request != null ? request.url : "");
}
return !((VersionedModel) model).optimisiticLockingViolated;
}
}
You use this Check with the annotation @CheckWith(value=OptimisticLockingCheck.class, message="optimisticLocking.modelHasChanged")
So lets have a closer look how it works. The only thing we have to do is to extends the class play.data.validation.Check and overwrite the isSatisfied method. There you get your model and the value of the properties. All you have to do is to return true if everything is OK or false otherwise. In our case we want to set the current url as a parameter. This can be easily done by calling setMessage(). We give the message or the message key which is defined in the messages properties and the parameters. Remember we only give 1 parameter but referred as with %2$s, because the first parameter is always the name of the property.
Now the complex way based on the Range-check of play: First we need to define an Annotation
/**
* This field must be lower than and greater than.
* Message key: validation.range
* $1: field name
* $2: min reference value
* $3: max reference value
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Constraint(checkWith = RangeCheck.class)
public @interface Range {
String message() default RangeCheck.mes;
double min() default Double.MIN_VALUE;
double max() default Double.MAX_VALUE;
}
and then the Check
@SuppressWarnings("serial")
public class RangeCheck extends AbstractAnnotationCheck<Range> {
final static String mes = "validation.range";
double min;
double max;
@Override
public void configure(Range range) {
this.min = range.min();
this.max = range.max();
setMessage(range.message());
}
public boolean isSatisfied(Object validatedObject, Object value, OValContext context, Validator validator) {
requireMessageVariablesRecreation();
if (value == null) {
return true;
}
if (value instanceof String) {
try {
double v = Double.parseDouble(value.toString());
return v >= min && v <= max;
} catch (Exception e) {
return false;
}
}
if (value instanceof Number) {
try {
return ((Number) value).doubleValue() >= min && ((Number) value).doubleValue() <= max;
} catch (Exception e) {
return false;
}
}
return false;
}
@Override
public Map<String, String> createMessageVariables() {
Map<String, String> messageVariables = new TreeMap<String, String>();
messageVariables.put("2-min", Double.toString(min));
messageVariables.put("3-max", Double.toString(max));
return messageVariables;
}
}
OK I think the annotation don't must be explained. Lets have look on the check. In this case it's extends net.sf.oval.configuration.annotation.AbstractAnnotationCheck
. We have to write a configure-method where we get the annotation and can copy the parameters. Then we have to define our check. Which is analog to the implementation of the other check. So we only write our condition and return true or false, except one special line! If we used a parametrized message, we must call requireMessageVariablesRecreation();
in our method.
At least we must override the method createMessageVariables
. Here we have to get a littlebit play-knowlegde (all the other stuff is described here). You put your messages into an map with a key and value, but play only takes the values (see ValidCheck.java
in framework code). So it will be referenced by position. This is the reason I changed the implementation of the RangeCheck
using TreeMap
instead of HashMap
. Furthermore I let the keys start with the index which they can referred.
So I hope this makes it more clear how to write custom validations/checks for play. I hope the description is correct. Therefor the question is my understanding correct?