(JSF validators/converters, Mojarra UIInput, immediate attr)
How many articles about JSF converters/validators have you read ? Many?
How many articles about JSF converters/validators have you read ? Many?
How many of them were good? A few!
Is this article good? I hope!
If you are not a novice, then there is no secret how the JSF
converters and validators works. But, if you think that it will be nice to have
a concise remainder, or is a topic like a tongue-twister nursery rhyme, then,
as a JSF developer, is important to understand the below paragraphs.
The idea behind converters/validators consist in providing a
layer between the user interface and the data model. More precisely, the same
information is represented in a readable and modifiable manner to the user, and
it is stored in the data model using data types, like int, String,
Date,
etc. Per example, we may have a java.util.Date object stored in the data
model that is exposed to the user as a simple text. The user can read the date
and modify it accordingly (he can't modify the java.util.Date object from a
web interface, but it can modify the information extracted from the object).
After he submits the form that contains the modified date, JSF should capture
the text and convert it back to an java.util.Date object. Further, the java.util.Date
object can participate in different kind of operations that involves dates
(e.g. build a graph, make a schedule etc - for these tasks is much more useful
to work with java.util.Date
objects, instead of dates as strings).
Note When we are working with objects without
using a converter, the end-user will see the result of calling the object.toString() method.
Moreover, JSF should validate the result of conversion to
ensure that some internal requirements of the application are respected (e.g.
accept only dates after 1 January 2000). Finally, the information is pushed
back in data model.
But, what is actually happening ? When a form is submitted,
the browser is responsible to send a request
value to the server (you can easily inspect the request values using
Firebug). Once the request value reach the server side, JSF will store it in a
component object in the phase called Apply Request Values. At this moment the
request value becomes the submitted
string value. Further, in the Process Validation phase, the submitted
string value is converted to a new value
of the appropriate object type that is required by the application (e.g. java.util.Date).
Next, JSF will apply the validators to the new value (if they are any). If the new
value is valid, it becomes the local
value which can be used to update the model.
Of course, conversion and validation will not work smoothly
all the time! When they failed (any of them) JSF takes a radical decision and
"rejects" the user request by invoking the Render Response phase,
which is responsible to redisplay the current page. Page authors are aware of
this behavior, so they have to prepare the terrain by "infiltrating"
in page tags as, <h:message> and <h:messages>. These
tags will reveal the info, warning and error messages that has been fired by
the converters/validators. In such way, the user is informed about what went
wrong, and he can supply another request values. The process repeats until the
Process Validation phase is successfully ended. Next, is time to update the
data model using the converted/validated data. This take place in the Update
Model phase.
In order to "disturb" a little the above sequence
of events, we have to use important attributes that can be applied for input
components. The first one is the required attribute - basically, this is a
flag that specify whether a value must be supplied for a UIInput
component or a component that extends UIInput (e.g. UIViewParameter). Per
example, for an UIInput,
the required value is a non-empty value (non-empty string, non-empty List
or non-null),
but the submitted null values tells JSF that the component was not
submitted at all, which means that it doesn't apply the required validator. So,
if the submitted value was not null, and it successfully passed the
conversion, then the required validator will stop an empty value to be validated
by other attached validators. When required="false", JSF will pass
the empty value through the attached validators if the javax.faces.VALIDATE_EMPTY_FIELDS
context parameter was set to true (default true, since JSF 2.0) and the
submitted value was not null. But, keep in mind that setting this
context parameter to false, will decrease the power of JSR 303 Bean Validation
usability. The same logic is applicable in case of the UIViewParameter, only
that in this case a submitted null value (not empty string) for a view
parameter will suddenly stop the validation and the response is rendered. This
is a decision specific to UIViewParameter, which applies the required
validator earlier than UIInput. For a better understanding, think
that if we have an attached converter, then the UIViewParameter applies
required validator to submitted value, and to the converted value, while the UIInput
applies the required validator only to the converted value. Well, you may think
of required validator as " the guardian" of the validation process.
Practically, if you can't pass successfully over its control, nothing happens
further. The value is marked as invalid, an error message is placed in the
queue, and if there are more validators waiting, they are not called and the
page is redisplayed in Render Response phase. Obviously, if you don't use this
validator, then you have to deal with empty values in a custom way.
The other important attribute is immediate. This is
another flag that indicates that the component's
value must be converted and validated immediately, during the Apply Request
Values phase, rather than waiting until Process Validations phase. Most
probably, you will see this attribute used in command components (e.g. <h:commandButton>)
to implement buttons like, "Cancel" and "Clear". This is
needed for transferring the invocation of action in Apply Request Values phase.
Now, let's have a quick overview of this flow from the
methods perspective. We can mark as the starting point the processDecodes()
method. This method is implemented in the UIComponentBase, and is adjusted in specific input components, like UIInput and UIViewParameter. By default, in UIComponentBase, "this method perform the component tree processing required by the Apply Request Values phase of the request processing lifecycle for all facets of this component, all children of this component, and this component itself". Notice below the relevant part:
...
In case of UIInput, this method is called in the Apply Request Values phase and it inspects the value of the immediate attribute. If this value is true, then it triggers the validation before the Process Validations Phase:
...
pushComponentToEL(context, null);
Iterator kids = getFacetsAndChildren();
while (kids.hasNext()) {
UIComponent kid = (UIComponent)
kids.next();
kid.processDecodes(context);
}
try {
decode(context);
} catch (RuntimeException e) {
context.renderResponse();
throw e;
} finally {
popComponentFromEL(context);
}
...
In case of UIInput, this method is called in the Apply Request Values phase and it inspects the value of the immediate attribute. If this value is true, then it triggers the validation before the Process Validations Phase:
public void
processDecodes(FacesContext context) {
...
super.processDecodes(context);
if
(isImmediate()) {
executeValidate(context);
}
}
By the other hand the UIViewParameter doesn't override this method (immediate is always false).
Further, if the immediate is false, then in Process
Validations phase, the processValidators()
method is called. This method is implemented in the UIComponentBase, and
is adjusted in specific input components, like UIInput and UIViewParameter.
By default, in UIComponentBase, "this method perform the component tree
processing required by the Process Validations phase of the request processing
lifecycle for all facets of this component, all children of this component, and
this component itself". Notice below the relevant part:
...
pushComponentToEL(context,
null);
Application
app = context.getApplication();
app.publishEvent(context,
PreValidateEvent.class, this);
Iterator kids = getFacetsAndChildren();
while (kids.hasNext()) {
UIComponent kid = (UIComponent) kids.next();
kid.processValidators(context);
}
app.publishEvent(context,
PostValidateEvent.class, this);
popComponentFromEL(context);
...
In UIInput, this behavior is adjusted to take into account
the immediate
attribute. Notice below the relevant part:
...
if (!isImmediate()) {
Application application = context.getApplication();
application.publishEvent(context, PreValidateEvent.class, this);
executeValidate(context);
application.publishEvent(context, PostValidateEvent.class, this);
}
for (Iterator<UIComponent> i =
getFacetsAndChildren(); i.hasNext(); ) {
i.next().processValidators(context);
}
...
Further, in UIViewParameter the above implementation is
adjusted to deal with null values in presence of required validator. Well, UIInput
assumes that a null
value means don't check, but UIViewParameter doesn't accept null
values and required
validator:
...
if (getSubmittedValue() == null &&
myIsRequired()) {
String requiredMessageStr =
getRequiredMessage();
FacesMessage message;
if (null != requiredMessageStr) {
message = new
FacesMessage(FacesMessage.SEVERITY_ERROR,
requiredMessageStr,
requiredMessageStr);
} else {
message =
MessageFactory.getMessage(context, REQUIRED_MESSAGE_ID,
MessageFactory.getLabel(context,
this));
}
context.addMessage(getClientId(context), message);
setValid(false);
context.validationFailed();
context.renderResponse();
} else {
super.processValidators(context);
}
...
The myIsRequired() method is a private method
that checks the well-known isRequired() and another private
method, named isRequiredViaNestedRequiredValidator()
- its name is self explanatory:
private
boolean myIsRequired() {
return super.isRequired() ||
isRequiredViaNestedRequiredValidator();
}
So, when there are non-null submitted values or the
required validator is not used, the UIViewParameter relies on UIInput
implementation. It is easy to observe that the flow goes into the executeValidate()
method. This is a private method defined in UIInput, which basically
calls the validate()
method, and afterwards, if the validation failed, it renders the response:
private void
executeValidate(FacesContext context) {
try {
validate(context);
} catch (RuntimeException e) {
context.renderResponse();
throw e;
}
if
(!isValid()) {
context.validationFailed();
context.renderResponse();
}
}
So, the validate() method is the "brain" of
validation. This is the place where the magic happens. First, if the submitted
value is null,
the validation returns. JSF consider that
the component was not submitted
at all. In case of an UIViewParameter, at this point, we know that
the required
is false.
...
Object submittedValue = getSubmittedValue();
if
(submittedValue == null) {
return;
}
...
Further, JSF deals the empty strings, depending on the
context parameter, javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL,
which is false
by default:
...
if ((considerEmptyStringNull(context)
&& submittedValue instanceof String
&& ((String) submittedValue).length() == 0)) {
setSubmittedValue(null);
submittedValue = null;
}
...
One step further, and the converter's getAsObject() method
is called (via getConvertedValue()
method). It will convert the submitted string value to the object type required
by the application - if the component is bound to a bean property in the model
, but there is no converter attached, then JSF will use the converter that has
the same data type as the bean property. If conversion fails, the submitted
value is marked as invalid and JSF adds an error message to the queue
maintained by the FacesContext. Further, the flow reach the validateValue()
method, which validates the converted value (newValue):
...
Object newValue = null;
try {
newValue = getConvertedValue(context, submittedValue);
} catch
(ConverterException ce) {
addConversionErrorMessage(context, ce);
setValid(false);
}
validateValue(context, newValue);
...
But, what's happening inside the validateValue() method
? Well, first, if the new value is valid (it successfully passed the converter)
and it is an empty value (empty string, empty List or null)
and the required validator is used, then the new value is marked as invalid and
an error message is placed in the queue of FacesContext. But, if the
new value is valid and it is not an empty value or is an empty value, but
validate empty fields is enabled, then calls all validators, one by one.
If validation is successfully accomplished, then the new
value is stored as the local value, and it is used later to update the model,
the submitted value is deleted (set to null) and the ValueChangeEvent is
emitted (if it has to):
...
if
(isValid()) {
Object previous = getValue();
setValue(newValue);
setSubmittedValue(null);
if
(compareValues(previous, newValue)) {
queueEvent(new ValueChangeEvent(this, previous, newValue));
}
}
...
Finally, the flow reaches the Update Model Values phase and
the local value is used to update the data model. JSF needs those local values
to keep the data until the entire validation process is successfully done. The
local values "gain" the access to the data model only if all
validations has successfully passed.
Maybe, we pushed the things a little bit too far, but
sometimes, in order to fit the puzzle pieces, is good to have the big picture
in mind. So, here it is the picture of
how validators and converters works in Mojarra 2.2 for UIInput:
Niciun comentariu :
Trimiteți un comentariu