Let's
suppose that you want to write a custom component that should support a JSF
converter (custom converter or built-in converter). In this post, you will see the
OmniFaces technique for accomplishing this task, but, first you have to know
the main steps that a custom component should respect in order to support the converter
attribute, <f:converter>
tag or <f:convertNumber>,
etc:
·
implement the ValueHolder interface
·
override the ValueHolder.setConverter()
method
·
override the ValueHolder.getConverter()
method
·
override the rest of methods (not discussed in
this post)
So, we can
have a quick skeleton of such a custom component:
import
javax.faces.component.FacesComponent;
import
javax.faces.component.ValueHolder;
import
javax.faces.convert.Converter;
...
@FacesComponent(...)
public class
MyComponent implements ValueHolder {
private enum PropertyKeys {
// Cannot be uppercased. They have to exactly
match the attribute names.
converter; // converter attribute name
}
private Converter converter; // converter object
@Override
public Converter getConverter() {
// return the converter
}
@Override
public void setConverter(Converter converter)
{
// set the converter
}
// override rest of methods
}
So, via the converter
object we can call the well-known getAsObject() and getAsString()
methods, but, before that we need to ensure that it points a Converter instance.
Based on a simple intuition, you may think that getConverter() and setConverter()
implementations can be like below, and, since we are implementing the ValueHolder,
JSF will call these methods accordingly:
...
@Override
public Converter getConverter() {
return converter;
}
@Override
public void setConverter(Converter converter)
{
this.converter = converter;
}
...
Well, is
true that in some cases this implementation is ok, but, let's focus on the setConverter()
method, and let's analyze when JSF calls it:
·
built-in converter indicated via <f:convertNumber>
<t:myComponent>
<f:convertNumber
type="percent"/>
</t:myComponent>
This will call
the setConverter()
method. In Mojarra, the method will be called via ConverterTagHandlerDelegateImpl#applyAttachedObject()
method.
·
built-in converter indicated via <f:converter>
as string
<t:myComponent>
<f:converter
converterId="javax.faces.BigInteger"/>
</t:myComponent>
This will call
the setConverter()
method. In Mojarra, the method will be called via ConverterTagHandlerDelegateImpl#applyAttachedObject()
method.
·
built-in converter indicated via <f:converter>
as ValueExpression
private
String bigConverter= "javax.faces.BigInteger";
...
<t:myComponent>
<f:converter
converterId="#{myBean.bigConverter}"/>
</t:myComponent>
This will call
the setConverter()
method. In Mojarra, the method will be called via ConverterTagHandlerDelegateImpl#applyAttachedObject()
method.
·
custom converter indicated via <f:converter>
as string
<t:myComponent>
<f:converter converterId="myConverter"/>
</t:myComponent>
This will call
the setConverter()
method. In Mojarra, the method will be called via ConverterTagHandlerDelegateImpl#applyAttachedObject()
method.
·
custom converter indicated via <f:converter>
as ValueExpression
private
String myConverter= "myConverter";
...
<t:myComponent>
<f:converter
converterId="#{myBean.myConverter}"/>
</t:myComponent>
This will call
the setConverter()
method. In Mojarra, the method will be called via ConverterTagHandlerDelegateImpl#applyAttachedObject()
method.
·
built-in converter indicated via converter
attribute as string
<t:myComponent
converter="javax.faces.BigInteger" />
This will call
the setConverter()
method. In Mojarra, the method will be called via ValueHolderRule$LiteralConverterMetadata#applyMetadata()
method.
·
built-in converter indicated via converter
attribute as ValueExpression
private
Converter bigConverter= new BigIntegerConverter();
...
<t:myComponent
converter="#{myBean.bigConverter}" />
This will NOT call the setConverter() method.
In Mojarra, this case is resolved by the ValueHolderRule$DynamicConverterMetadata2#applyMetadata()
method.
·
custom converter indicated via converter
attribute as string
<t:myComponent
converter="myConverter" />
This will call
the setConverter()
method. In Mojarra, the method will be called via ValueHolderRule$LiteralConverterMetadata#applyMetadata()
method.
·
custom converter indicated via converter attribute
as ValueExpression
private
Converter myConverter= new MyConverter();
...
<t:myComponent
converter="#{myBean.myConverter}" />
This will NOT call the setConverter() method.
In Mojarra, this case is resolved by the ValueHolderRule$DynamicConverterMetadata2#applyMetadata()
method.
We can easily conclude that the setConverter() method is not
called when the converter is indicated via a ValueExpression that must
evaluate to javax.faces.convert.Converter.
This will make our getConverter() to return null! This is happening because
JSF distinguish between static
converters (indicated via converter ID) and dynamic
converters (indicated via ValueExpression evaluated to javax.faces.convert.Converter).
For the static converters JSF will
invoke the setConverter()
method, while for dynamic converters,
JSF will only invoke the setValueExpression() method which ultimately
stores the EL expression in state helper. The getConverter() then
evaluates it and assigns the result as an instance variable as they are not
necessarily serializable (and thus should not be stored in state helper!).
Let's check out the Mojarra code for the case of using <f:converter>
tag. Look how the ConverterTagHandlerDelegateImpl#applyAttachedObject()
method calls the setConverter():
// Mojarra
2.2.9 - com.sun.faces.facelets.tag.jsf. ConverterTagHandlerDelegateImpl
public void
applyAttachedObject(FacesContext context, UIComponent parent) {
FaceletContext ctx = (FaceletContext)
context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
ValueHolder vh = (ValueHolder) parent;
ValueExpression ve = null;
Converter c = null;
if (owner.getBinding() != null) {
ve =
owner.getBinding().getValueExpression(ctx, Converter.class);
c =
(Converter) ve.getValue(ctx);
}
if (c == null) {
c = this.createConverter(ctx);
if (ve != null) {
ve.setValue(ctx, c);
}
}
if (c == null) {
throw new TagException(owner.getTag(),
"No Converter was created");
}
owner.setAttributes(ctx, c);
vh.setConverter(c);
Object lv = vh.getLocalValue();
FacesContext faces = ctx.getFacesContext();
if (lv instanceof String) {
vh.setValue(c.getAsObject(faces, parent,
(String) lv));
}
}
private
Converter createConverter(FaceletContext ctx) {
if (owner.getConverterId(ctx) == null) {
throw new TagException(owner.getTag(),
"Default behavior invoked of
requiring a converter-id passed in the constructor, must override
ConvertHandler(ConverterConfig)");
}
return
ctx.getFacesContext().getApplication().createConverter(owner.getConverterId(ctx));
}
If you
fallow the code line by line, is pretty clear how it works.
Now, let's see the case of using the converter attribute,
which is more interesting for us. In Mojarra, the magic happens in ValueHolderRule
class as below:
// Mojarra
2.2.9 - com.sun.faces.facelets.tag.jsf.ValueHolderRule
final class
ValueHolderRule extends MetaRule {
final static class LiteralConverterMetadata extends
Metadata {
private final String converterId;
public LiteralConverterMetadata(String
converterId) {
this.converterId = converterId;
}
public void applyMetadata(FaceletContext ctx,
Object instance) {
((ValueHolder) instance).setConverter(ctx.getFacesContext()
.getApplication().createConverter(this.converterId));
}
}
...
final static class DynamicConverterMetadata2
extends Metadata {
private final TagAttribute attr;
public DynamicConverterMetadata2(TagAttribute
attr) {
this.attr = attr;
}
public void applyMetadata(FaceletContext ctx,
Object instance) {
((UIComponent)
instance).setValueExpression("converter", attr
.getValueExpression(ctx,
Converter.class));
}
}
...
public final static ValueHolderRule Instance =
new ValueHolderRule();
public
Metadata applyRule(String name, TagAttribute attribute, MetadataTarget meta) {
if
(meta.isTargetInstanceOf(ValueHolder.class)) {
if
("converter".equals(name)) {
if (attribute.isLiteral()) {
return new
LiteralConverterMetadata(attribute.getValue());
} else {
return new
DynamicConverterMetadata2(attribute);
}
}
...
}
return null;
}
}
Finally, the UIComponent#setValueExpression() will access
the state via getStateHelper().put()
and will store the converter. Now, that we know how the things works, we can
adjust the getConverter()
accordingly. OmniFaces uses the follow getConverter() in its Param
component:
private Converter localConverter;
@Override
public
Converter getConverter() {
}
So, our
custom component skeleton will become:
import
javax.faces.component.FacesComponent;
import
javax.faces.component.ValueHolder;
import
javax.faces.convert.Converter;
...
@FacesComponent(...)
public class
MyComponent implements ValueHolder {
private enum PropertyKeys {
// Cannot be uppercased. They have to exactly
match the attribute names.
converter; // converter attribute name
}
private Converter localConverter; // converter object
@Override
public void setConverter(Converter converter) {
localConverter = converter;
}
public void setConverter(Converter converter) {
localConverter = converter;
}
@Override
public Converter getConverter() {
}
// override rest of methods
}
Done!
Niciun comentariu :
Trimiteți un comentariu