In OmniFaces custom components we can see many useful programming techniques. Per example, in this post, we will take a look on how the custom components are declared and grouped. This is especially useful for developers who want to see how to manage multiple components in large projects. Following the right techniques is important right from the start!
Starting with JSF 2.0, custom components can be created via @FacesComponent annotation, but the *taglib.xml file is still needed. In order to avoid this file, JSF 2.2 has added three more attributes to this annotation. Now, we can create a custom component without a *taglib.xml file by exploiting these attributes:
• createTag: This can be set to true or false. When it is set to true, JSF will
generate the tag for us (to be more specific, JSF will create, at runtime, a
Facelet tag handler that extends ComponentHandler). This element can be
used only in JSF 2.2.
• tagName: This allows us to indicate the tag name. When createTag is set to
true, JSF will use this name for the generated tag. This element can only be
used in JSF 2.2.
• namespace: This allows us to indicate the tag namespace. When createTag
is set to true, JSF will use this namespace for the generated tag. When
namespace is not specified, JSF will use the http://xmlns.jcp.org/jsf/component namespace.
This element can be used only in JSF 2.2.
• value: This element comes from JSF 2.0 and indicates the component-type.
The component-type can be used as the argument of the Application.createComponent(java.lang.String) method for creating instances of the UIComponent class. JSF uses the component-type for creating components.
As of JSF 2.2, if the value element is missing or is null, JSF will obtain it by calling the getSimpleName() method on the class to which @FacesComponent is attached and lowercasing the first character.
So, starting
with JSF 2.2, the OmniFaces components (e.g. DeferredScript) should look like
this:
@FacesComponent(value=DeferredScript.COMPONENT_TYPE,
createTag=true, tagName="deferredScript", namespace="http://omnifaces.org/ui")
public
class DeferredScript extends ScriptFamily {
public static final String COMPONENT_TYPE =
"org.omnifaces.component.script.DeferredScript";
But, if you
check out the OmniFaces 2.0 components, you will see that, basically, each
component uses the @FacesComponent, like this (e.g. the DeferredScript
component):
@FacesComponent(DeferredScript.COMPONENT_TYPE)
public
class DeferredScript extends ScriptFamily {
public static final String COMPONENT_TYPE =
"org.omnifaces.component.script.DeferredScript";
Note The @FacesComponent(DeferredScript.COMPONENT_TYPE)
is just a shortcut for @FacesComponent(value=DeferredScript.COMPONENT_TYPE).
If more attributes are used, then the shortcut is useless, and the value is mandatory!
There is a
common practice to define the component-type
as a static final string directly in component class and to name it COMPONENT_TYPE. Some developers tend to place the component-type string directly in
annotation, which somehow restricts the programmatic access to this
information, since, by default, there is no public UIComponent.getComponentType()
method to override - the below example is not recommended, but it is not a big
issue if you have a single component (a didactical example):
@FacesComponent("org.omnifaces.component.script.DeferredScript")
@FacesComponent(value="org.omnifaces.component.script.DeferredScript")
If you check JSF components (or simply the
documentation), you will notice that all JSF components defines the COMPONENT_TYPE static field, except UIComponent and
UIComponentBase, which are abstract (generic) components. Next to this field,
we have the component-family field, named
COMPONENT_FAMILY. Per example, the
UIInput component has the following COMPONENT_TYPE and COMPONENT_FAMILY:
/**
* <p>The
standard component type for this component.</p>
*/
public static final String COMPONENT_TYPE = "javax.faces.Input";
/**
* <p>The
standard component family for this component.</p>
*/
public static final String COMPONENT_FAMILY =
"javax.faces.Input";
If you
extend UIInput you will inherit its type and family, but if you extend
UIComponentBase, then you need to explicitly provide the COMPONENT_TYPE and
COMPONENT_FAMILY. You just saw above how OmniFaces provides the COMPONENT_TYPE
for one of its components. Before we focus on COMPONENY_FAMILY, let's notice
that, OmniFaces uses the JSF 2.2 @FacesComponent at minimum power. The
@FacesComponent annotation and the component-type are mandatory. If a
custom component doesn't use @FacesComponent and doesn't define its
component-type, then you will see an error like this:
Expression Error: Named Object: ... not
found.
The alternative
to @FacesComponent can be added in faces-config.xml (this declarative approach
has higher priority than @FacesComponent) - in Mojarra, the faces-config.xml is named jsf-ri-runtime.xml and it is available in com.sun.faces folder (JSF doesn't uses @FacesComponent ):
<component>
<component-class>component-class</component-class>
<component-type>component-type</component-type>
</component>
Per example, in Mojarra, for UIViewParameter, we have:
<component
xmlns:xi="http://www.w3.org/2001/XInclude">
<component-class>javax.faces.component.UIViewParameter</component-class>
<component-type>javax.faces.ViewParameter</component-type>
...
</component>
The @FacesComponent annotation
registers the components with the JSF implementation, while the component-type is used to create the
component by Application.createComponent() methods.
Since JSF
2.2, the component-type can be
omitted in @FacesComponent, because JSF will determine it like this
(ComponentConfigHandler class):
...
String value = ((FacesComponent) annotation).value();
if (null == value || 0 == value.length()) {
value = target.getSimpleName();
value =
Character.toLowerCase(value.charAt(0)) + value.substring(1);
}
...
Per example,
if the OmniFaces DeferredScript had been declared like this:
@FacesComponent
public
class DeferredScript extends ScriptFamily {
then you
could programmatically have:
FacesContext.getCurrentInstance().getApplication().createComponent("deferredScript");
But, in
order to inform Facelets, in the *taglib.xml, you need this - when you do this, you instruct Facelets to create a component of the given component-type:
<tag>
...
<component>
<component-type>deferredScript</component-type>
</component>
...
</tag>
When you have many custom components, as OmniFaces contains, this is not recommended. If more than one component with this derived name is found, the behavior is undefined. Usually, the component-type is of type (org.omnifaces.component.output.GraphicImage, javax.faces.Input, javax.faces.ViewRoot, etc). Maybe, in the future, OmniFaces will provide an alternative to getSimpleName().
...
</tag>
When you have many custom components, as OmniFaces contains, this is not recommended. If more than one component with this derived name is found, the behavior is undefined. Usually, the component-type is of type (org.omnifaces.component.output.GraphicImage, javax.faces.Input, javax.faces.ViewRoot, etc). Maybe, in the future, OmniFaces will provide an alternative to getSimpleName().
In order to
avoid the *taglib.xml, you need to use the createTag, tagName and namespace
attributes also. But, notice that these attributes are not used by OmniFaces. Well,
there are multiple explanations for this. Per example, let's take the namespace
attribute; since OmniFaces contains many custom components, it should add this
attribute for each of them. But, if someday it decides to change that
namespace (http://omnifaces.org/ui) then
it has to perform the modification in all components, which is not desirable.
Further, let's think that most tags has attributes, and some of them may be
required. We can't mark the required attributes in component source code.
Required attributes are defined in *taglib.xml (e.g. Omnifaces use this style):
<tag>
...
<attribute>
<description>
<![CDATA[
The
"resource name" part of the resource identifier.
]]>
</description>
<name>name</name>
<required>true</required>
<type>java.lang.String</type>
</attribute>
...
</tag>
</tag>
JSF reference
implementation, Mojarra is using*taglib.xml also. In Mojarra, the *taglib.xml
files are located under com.sun.faces.metadata.taglib.
Is true that
this approach will not cause JSF to perform a check at runtime, and is strongly
linked to the editor type. NetBeans IDE will cause an error, while Eclipse doesn't.
By the other hand, you can exploit the ComponentHandler.getRequiredAttribute()
method, which will perform a check at runtime and will throw a TagException if
the attribute was not found, but this means to declare a ComponentHandler for
each component and to configure it in *taglib.xml via <handler-class>
tag.
Moreover, without
*taglib.xml, IDEs (e.g. NetBeans) cannot provide a solid documentation and
auto-completion for these tags. So using createTag and tagName in
@FacesComponent will not provides us the above advantages. In addition, as we
said earlier, a custom component may need a custom component handler, which
cannot be defined in @FacesComponent, and only in *taglib.xml via
<handler-class> tag. So, if you are in case of creating a bulk of
components, like OmniFaces, then you better rely on *taglib.xml, and not
entirely on @FacesComponent. OmniFaces components are defined in
omnifaces-ui.taglib.xml file
(e.g. in OmniFaces 2.0 - https://github.com/omnifaces/omnifaces/blob/2.0/src/main/resources/META-INF/omnifaces-ui.taglib.xml).
But, for testing purposes, or in house components used in different small
applications, is not bad to try the @FacesComponent at full capacity and avoid using the *taglib.xml files.
Another
important aspect is how to group components ? We know that each component is
rendered after its component-family and renderer-type passes through the RenderKit.getRenderer()
method:
public abstract Renderer getRenderer(java.lang.String
family,
java.lang.String rendererType)
This method
match the correct renderer , like this:
private ConcurrentHashMap<String, HashMap<String,
Renderer>> rendererFamilies =
new
ConcurrentHashMap<String, HashMap<String, Renderer>>();
...
HashMap<String,Renderer> renderers =
rendererFamilies.get(family);
return ((renderers != null) ? renderers.get(rendererType) :
null);
So, in order
to obtain its renderer, each component must reveal its family
(COMPONENT_FAMILY) and renderer-type to this method (with a simple custom
RenderKit you can check out the JSF/OmniFaces components families and renderer
types). Programmatically, a family, component-family,
is obtained via UIComponent.getFamily(), and the renderer-type via UIComponent.getRendererType():
public abstract java.lang.String getFamily()
public abstract java.lang.String getRendererType()
Now, JSF search
through available renderers that was added via RenderKit.addRenderer(). JSF has inspected faces-config.xml file for:
<render-kit>
<renderer>
<component-family>component-family</component-family>
<renderer-type>renderer-type</renderer-type>
<renderer-class>RendererClass</renderer-class>
</renderer>
</render-kit>
and all classes annotated with @FacesRenderer:
@FacesRenderer(componentFamily=ComponentClass.COMPONENT_FAMILY,
rendererType= RendererClass.RENDERER_TYPE)
public class RendererClass extends Renderer {
public static final String RENDERER_TYPE = "renderer-type";
Optionally, Facelets
can be also informed by the render type in *taglib.xml. When you do that, you instruct Facelets to create a component of the given component-type. The component class is annotated with @FacesComponent or has been definied in faces-config.xml. In addition Facelets will set to the given renderer type.
<tag>
...
<component>
<component-type>component-type</component-type>
<renderer-type>renderer-type</renderer-type>
</component>
...
</tag>
...
</tag>
Note There
is no <component-family> tag! That is known from the getFamily() method.
Note Is
important to know that you cannot override the default JSF renderers using the @FacesRenderer annotation. The default renderer is applied, except
if you are using the declarative approach.
Note A
Renderer is not selected based on the component-type and renderer-type!
Is selected based on component-family and renderer-type, which allows a
renderer to be used for multiple components in the same family. The component-type is used for creating components in view root!
Now, when
you need to create multiple components, is a good practice to group their
common characteristics under an abstract class . Per example, the OmniFaces
components: DeferredScript, Highlight and OnloadScript share the abstract component,
ScriptFamily:
package org.omnifaces.component.output;
import javax.faces.component.UIComponentBase;
public abstract class OutputFamily extends UIComponentBase
{
/** The standard component family. */
public static final String COMPONENT_FAMILY =
"org.omnifaces.component.output";
@Override
public String
getFamily() {
return
COMPONENT_FAMILY;
}
@Override
public boolean
getRendersChildren() {
return true;
}
}
Next, each
of DeferredScript, Highlight and OnloadScript components extends the ScriptFamily
and inherits the same family and the fact that they will render their children.
So, when you have multiple components that share common artifacts, is
recommended to follow this technique, and instead of repeating code in each
component, you may declare an abstract component as above. This will allow you
to change common artifacts with just a snap of a finger!
In the below figure, you can see how the component-type,
component-family and renderer-type are related in OmniFaces, DeferredScript
component (I know that it seems complicated, but if you take a deep breath and
focus a little, this figure can clear up many important aspects):
Sometimes, a
component render itself (it doesn't have an explicit Renderer) and extends
another existent component. An interesting case is represented by the
OmniFaces, CommandScript component, which extends the UICommand component, but
it belongs to the Script family, not to the javax.faces.Command family. Since it extends the UICommand, it cannot
extends the ScriptFamily also, so it needs to override the
UICommand.getFamily() method:
@Override
public String getFamily() {
return
ScriptFamily.COMPONENT_FAMILY;
}
Moreover,
the CommandScript doesn't have an external Renderer, so it has to override the setRendererType()
as below:
setRendererType(null);
The
techniques revealed here are simple, but very useful when you think to start to a large project. Is nice to see that OmniFaces provides us the guarantee that we
can use these techniques in our projects, without having any doubts!
Niciun comentariu :
Trimiteți un comentariu