Read also:
·
direct
implementation model -UI components implementation is also responsible to
render themselves;
·
delegated
implementation model - UI components implementation delegates the rendering
process to a separate renderer associated with the component.
The second
approach (delegated implementation model) is our centerpiece. For example,
check out the JSF component UISelectBoolean source code. Almost
immediately you can notice that this component has a rendered identified as javax.faces.Checkbox
- the setRendererType()
method instructs JSF which renderer should be associated with the current
component; when we pass null, it instruct JSF that the component
belongs to direct implementation model:
// Mojarra
2.2.9 source code - javax.faces.component.UISelectBoolean
public class
UISelectBoolean extends UIInput {
public static final String COMPONENT_TYPE =
"javax.faces.SelectBoolean";
public static final String COMPONENT_FAMILY =
"javax.faces.SelectBoolean";
public UISelectBoolean() {
super();
setRendererType("javax.faces.Checkbox");
}
...
}
Further, the
renderer associated with this component is named, CheckboxRenderer:
// Mojarra
2.2.9 source code - com.sun.faces.renderkit.html_basic.CheckboxRenderer
public class
CheckboxRenderer
extends HtmlBasicInputRenderer {
...
@Override
public void decode(FacesContext context,
UIComponent component) {
...
}
@Override
public void encodeBegin(FacesContext context,
UIComponent component)
...
}
...
}
Well, this
model leverages the strategy design pattern. Mainly, a renderer can be seen as
an algorithm specialized in rendering a component. But, JSF allows us to have
(register) multiple such algorithms for a single component (multiple renderers)
and to dynamically vary the algorithm. Multiple renderers are registered via
JSF,
RenderKit. At application startup time, JSF implementation parses the configuration file (faces-config.xml)
and associates those renderers to the UI components.
For a better
understanding, let's see how we can follow the strategy pattern and the JSF API
constraints to write custom renderers.
Write Custom Renderer Skeleton
What is the main goal of a renderer ?
· Is responsible to decode the values from the incoming request and to encode the values to be displayed by transforming the component tree into the HTML markup that will be displayed to the client machine. Shortly, to transform a JSF component in markup (e.g. HTML, XML, etc).
When you commonly need a custom renderer ?
· when you need to render a custom component (brand new or extension of a built-in one), because none of the built-ins do what you want to achieve
· when you want to alter the look/functionality of an built-in component
What should I know before start writing a custom renderer ?
· mainly you need to know that a renderer extends directly/indirectly the Renderer class
· starting with JSF 2.2, you can extend RendererWrapper, which is a simple implementation of Renderer. JSF 2.2 comes with many wrappers, which are simple implementations of what they are wrapping, and they help developer to override only the necessary methods, and to provide specialized behavior to an existing wrapped instance (e.g. RendererWrapper can be used for providing specialized behavior to an existing Renderer instance). The wrapped instance is available via thegetWrapped() method.
· The main three methods of a Renderer are encodeBegin(), encodeChildren() and encodeEnd(). By default, JSF calls them in this order, and the first one usually renders the beginning of the markup - "open" tags (e.g. <head>, <input>, <form>, etc), the second one renders the children (this is configurable viagetRendersChildren() flag), and the last one is ending the markup - "close" tags (e.g. </head>, </input>, </form>).
· in order to link a component with a renderer, you should know how to work with the UIComponent.setRenderType() method and with the component-family,component-type and renderer-type artifacts as level of annotations or with the <render-kit> and <renderer> tags in faces-config.xml. A detailed dissertation about them is the OmniFacesCustom Components Approach post. OmniFaces provides solid techniques for writing custom components and renderers!
How do I usually write a Renderer skeleton ?
· when you write a brand new component (extending UIComponentBase), you will extend the Renderer class directly and override most of its methods. Usually in these cases you will link the custom component with the renderer via annotations and setRendererType() method.
DummyComponent
@FacesComponent(value = DummyComponent.COMPONENT_TYPE, createTag = true)
public class DummyComponent extends UIComponentBase {
public static final String COMPONENT_FAMILY = "jsf.components";
public static final String COMPONENT_TYPE = "jsf.components.DummyComponent";
public DummyComponent() {
setRendererType(DummyRenderer.RENDERER_TYPE);
}
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
}
DummyRenderer
@FacesRenderer(componentFamily = DummyComponent.COMPONENT_FAMILY, rendererType = DummyRenderer.RENDERER_TYPE)
public class DummyRenderer extends Renderer {
public static final String RENDERER_TYPE = "jsf.renderers.DummyRenderer";
private static final Logger LOG = Logger.getLogger(DummyRenderer.class.getName());
public DummyRenderer(){
}
@Override
public Object getConvertedValue(FacesContext context, UIComponent component, Object submittedValue) throws ConverterException {
return super.getConvertedValue(context, component, submittedValue);
}
@Override
public boolean getRendersChildren() {
return super.getRendersChildren();
}
@Override
public String convertClientId(FacesContext context, String clientId) {
return super.convertClientId(context, clientId);
}
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeBegin() method ---");
super.encodeBegin(context, component);
}
@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeChildren() method ---");
super.encodeChildren(context, component);
}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeEnd() method ---");
super.encodeEnd(context, component);
}
@Override
public void decode(FacesContext context, UIComponent component) {
LOG.info("--- Rendering DummyComponent - decode() method ---");
super.decode(context, component);
}
}
· when you write a custom component which extends a built-in component, you usually extend the renderer of the built-in component also - directly (most probably) or indirectly. Usually in these cases you will link the custom component with the renderer via the setRenderType() method. Of course, you can also use the renderer of the built-in component without any modifications.
DummyComponent
@FacesComponent(createTag = true)
public class DummyComponent extends UIOutput {
// inherits the UIOutput component-family: javax.faces.Output
//default renderer-type: javax.faces.Text is replaced by DummyRenderer
public DummyComponent() {
setRendererType(DummyRenderer.RENDERER_TYPE);
}
}
DummyRenderer
@FacesRenderer(componentFamily = DummyComponent.COMPONENT_FAMILY, rendererType = DummyRenderer.RENDERER_TYPE)
public class DummyRenderer extends TextRenderer {
private static final Logger LOG = Logger.getLogger(DummyRenderer.class.getName());
public static final String RENDERER_TYPE = "jsf.renderers.DummyRenderer";
public DummyRenderer(){
}
@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeChildren() method ---");
super.encodeChildren(context, component); // calls the TextRenderer#encodeChildren()
}
@Override
public boolean getRendersChildren() {
LOG.info("--- Rendering DummyComponent - getRendersChildren() method ---");
return super.getRendersChildren(); // calls the TextRenderer#getRendersChildren()
}
@Override
protected void getEndTextToRender(FacesContext context, UIComponent component, String currentValue) throws IOException {
LOG.info("--- Rendering DummyComponent - getEndTextToRender() method ---");
super.getEndTextToRender(context, component, currentValue); // calls the TextRenderer#getEndTextToRender()
}
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeBegin() method ---");
super.encodeBegin(context, component); // calls the TextRenderer#encodeBegin()
}
}
· when you just want to alter a built-in component at rendering level, you usually extend the built-in renderer and you instruct JSF to use your renderer instead of the default one via faces-config.xml, <renderer> tag.
DummyRenderer
public class DummyRenderer extends TextRenderer {
private static final Logger LOG = Logger.getLogger(DummyRenderer.class.getName());
public static final String RENDERER_TYPE = "jsf.renderers.DummyRenderer";
public DummyRenderer(){
}
@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeChildren() method ---");
super.encodeChildren(context, component); // calls the TextRenderer#encodeChildren()
}
@Override
public boolean getRendersChildren() {
LOG.info("--- Rendering DummyComponent - getRendersChildren() method ---");
return super.getRendersChildren(); // calls the TextRenderer#getRendersChildren()
}
@Override
protected void getEndTextToRender(FacesContext context, UIComponent component, String currentValue) throws IOException {
LOG.info("--- Rendering DummyComponent - getEndTextToRender() method ---");
super.getEndTextToRender(context, component, currentValue); // calls the TextRenderer#getEndTextToRender()
}
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeBegin() method ---");
super.encodeBegin(context, component); // calls the TextRenderer#encodeBegin()
}
}
faces-config.xml
...
<application>
<render-kit>
<renderer>
<component-family>javax.faces.Output</component-family>
<renderer-type>javax.faces.Text</renderer-type>
<renderer-class>jsf.renderers.DummyRenderer</renderer-class>
</renderer>
</render-kit>
</application>
...
Write Custom RenderKit Skeleton
What is the main goal of a RenderKit ?
· While the Renderer class converts the internal representation of UI components into markup (e.g. HTML), RenderKit represents a collection of Renderer instances capable to render JSF UI component's instances for a specific client (for example, a specific device). Each time JSF needs to render a UI component, it will call the RenderKit.getRenderer() method which is capable of returning an instance of the corresponding renderer based on two arguments that uniquely identifies it: the component-family and the renderer-type. Moreover, RenderKit registers renderers via RenderKit.addRenderer() method based on the component-family,renderer-type and Renderer instance. When we write a correct renderer (respecting the JSF specification) JSF will automatically find it and register it for us.
When you commonly need a custom RenderKit ?
· you may use a custom RenderKit to instruct JSF to delegate renderers in a specific approach. Per example, a custom RenderKit can choose the right renderer depending on device (PC, tablet, iPhone, etc). Or, you may have a custom Renderer that extends the RendererWrapper, and use a custom RenderKit to pass an instance of an existing Renderer to the custom Renderer. Or, you can write a custom RenderKit to add support for HTML5 specific attributes, like the OmniFaces Html5RenderKit (nevertheless, this will be removed starting with OmniFaces 3.0).
What should I know before start writing a custom RenderKit ?
· mainly, you need to know that a custom RenderKit extends directly/indirectly the RenderKit class
· starting with JSF 2.0, you can extend RenderKitWrapper, which is a simple implementation of RenderKit. Via RenderKitWrapper, you can provide a specialized behavior to an existing RenderKit instance. The wrapped instance is available via the getWrapped() method.
· The main two methods of a RenderKit are addRenderer()and getRenderer(). By overriding these methods, you can take control over the Renderers registration and delegation. Of course, there are many other useful methods listed in documentation.
How do I usually write a RenderKit skeleton ?
· usually, you will extend the RenderKitWrapper class, override the necessary methods, and configure it in the faces-config.xml via <render-kit> tag
DummyRenderKit
public class DummyRenderKit extends RenderKitWrapper {
private static final Logger LOG = Logger.getLogger(DummyRenderKit.class.getName());
private RenderKit renderKit;
public DummyRenderKit() {
}
public DummyRenderKit(RenderKit renderKit) {
LOG.log(Level.INFO, "Default RenderKit instance provided by JSF: {0}", renderKit.getClass().getSimpleName());
this.renderKit = renderKit;
}
@Override
public Renderer getRenderer(String family, String rendererType) {
LOG.log(Level.INFO, "--- Delegating renderer of type {0} for component in family {1} ---", new Object[]{rendererType, family});
return getWrapped().getRenderer(family, rendererType);
}
@Override
public void addRenderer(String family, String rendererType, Renderer renderer) {
LOG.log(Level.INFO, "--- Adding the renderer of type {0} for component in family {1} ---", new Object[]{rendererType, family});
getWrapped().addRenderer(family, rendererType, renderer);
}
@Override
public RenderKit getWrapped() {
return renderKit;
}
}
faces-config.xml
...
<render-kit>
<render-kit-class>
jsf.renderkits.DummyRenderKit
</render-kit-class>
</render-kit>
...
e.g. instruct JSF to render all components of a family via a common custom renderer (we simply apply a common CSS style to all components fromjavax.faces.Input family) (source code)
DummyRenderer
@ResourceDependencies({
@ResourceDependency(name = "css/styles.css", library = "default", target = "head")
})
@FacesRenderer(componentFamily = "javax.faces.all.Input", rendererType = DummyRenderer.RENDERER_TYPE)
public class DummyRenderer extends RendererWrapper {
private static final Logger LOG = Logger.getLogger(DummyRenderer.class.getName());
public static final String RENDERER_TYPE = "javax.faces.all";
private Renderer renderer;
public DummyRenderer() {
}
public DummyRenderer(Renderer renderer) {
this.renderer = renderer;
}
@Override
public boolean getRendersChildren() {
LOG.info("--- Rendering DummyComponent - getRendersChildren() method ---");
return getWrapped().getRendersChildren(); // calls the wrapped#getRendersChildren()
}
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeBegin() method ---");
getWrapped().encodeBegin(context, component); // calls the wrapped#encodeBegin()
}
@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeChildren() method ---");
getWrapped().encodeChildren(context, component); // calls the wrapped#encodeChildren()
}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeEnd() method [adding the common CSS style] ---");
ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.writeAttribute("class", "inputs", "class");
getWrapped().encodeEnd(context, component); // calls the wrapped#encodeChildren()
}
@Override
public Renderer getWrapped() {
return renderer;
}
}
DummyRenderKit
public class DummyRenderKit extends RenderKitWrapper {
private static final Logger LOG = Logger.getLogger(DummyRenderKit.class.getName());
private RenderKit renderKit;
public DummyRenderKit() {
}
public DummyRenderKit(RenderKit renderKit) {
LOG.log(Level.INFO, "Default RenderKit instance provided by JSF: {0}", renderKit.getClass().getSimpleName());
this.renderKit = renderKit;
}
@Override
public Renderer getRenderer(String family, String rendererType) {
// instruct JSF to use the DummyRenderer for each component that belongs to the "javax.faces.Input" family
if (family.equals("javax.faces.Input")) {
LOG.log(Level.INFO, "--- Delegating renderer of type {0} for component in family {1} ---", new Object[]{rendererType, family});
Renderer renderer = getWrapped().getRenderer(family, rendererType);
return new DummyRenderer(renderer);
}
return getWrapped().getRenderer(family, rendererType);
}
@Override
public RenderKit getWrapped() {
return renderKit;
}
}
e.g. instruct JSF to render UIOutput components via a custom renderer that was registered for other type of components, but pass to it an instance of the original renderer (source code)
DummyRenderer
@FacesRenderer(componentFamily = "javax.faces.cool.Output", rendererType = DummyRenderer.RENDERER_TYPE)
public class DummyRenderer extends RendererWrapper {
private static final Logger LOG = Logger.getLogger(DummyRenderer.class.getName());
public static final String RENDERER_TYPE = "javax.faces.cool.Text";
private Renderer renderer;
public DummyRenderer() {
}
public DummyRenderer(Renderer renderer) {
this.renderer = renderer;
}
@Override
public boolean getRendersChildren() {
LOG.info("--- Rendering DummyComponent - getRendersChildren() method ---");
return getWrapped().getRendersChildren(); // calls the TextRenderer#getRendersChildren()
}
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeBegin() method ---");
getWrapped().encodeBegin(context, component); // calls the TextRenderer#encodeBegin()
}
@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeChildren() method ---");
getWrapped().encodeChildren(context, component); // calls the TextRenderer#encodeChildren()
}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
LOG.info("--- Rendering DummyComponent - encodeEnd() method ---");
getWrapped().encodeEnd(context, component); // calls the TextRenderer#encodeEnd()
}
@Override
public Renderer getWrapped() {
return renderer;
}
}
DummyRenderKit
public class DummyRenderKit extends RenderKitWrapper {
private static final Logger LOG = Logger.getLogger(DummyRenderKit.class.getName());
private RenderKit renderKit;
public DummyRenderKit() {
}
public DummyRenderKit(RenderKit renderKit) {
LOG.log(Level.INFO, "Default RenderKit instance provided by JSF: {0}", renderKit.getClass().getSimpleName());
this.renderKit = renderKit;
}
@Override
public void addRenderer(String family, String rendererType, Renderer renderer) {
// notify when our custom dummy renderer is registered by JSF
if(family.equals("javax.faces.cool.Output") &&(rendererType.equals(DummyRenderer.RENDERER_TYPE))){
LOG.info("Your dummy renderer was registered !");
}
super.addRenderer(family, rendererType, renderer);
}
@Override
public Renderer getRenderer(String family, String rendererType) {
// instruct JSF to use the DummyRenderer instead of TextRenderer, but pass a TextRenderer instance to it
if (family.equals("javax.faces.Output") && (rendererType.equals("javax.faces.Text"))) {
LOG.log(Level.INFO, "--- Delegating renderer of type {0} for component in family {1} ---", new Object[]{rendererType, family});
Renderer renderer = getWrapped().getRenderer(family, rendererType);
return new DummyRenderer(renderer);
}
return getWrapped().getRenderer(family, rendererType);
}
@Override
public RenderKit getWrapped() {
return renderKit;
}
}
Write Custom RenderKitFactory Skeleton
What is the main goal of a RenderKitFactory ?
· it manages (register/provide) instances of available RenderKits.
When you commonly need a custom RenderKitFactory ?
· you may use a custom RenderKitFactory to instruct JSF to delegate RenderKits in a specific approach. By default, JSF has a single RenderKit, identified by an ID,HTML_BASIC_RENDER_KIT. But, if want to write another RenderKit then you can programmatically choose between them using a custom RenderKitFactory.
What should I know before start writing a custom RenderKitFactory ?
· mainly you need to know that a render kit factory extends directly/indirectly the RendererKitFactory class
· The main two methods of a RenderKitFactory are addRenderKit()and getRenderKit() . By overriding these methods, you can take control over the RenderKits8 registration and delegation. The render kits IDs can be obtained via getRenderKitIds() method. If this factory has been decorated, the implementation doing the decorating may override the getWrapped() method to provide access to the implementation being wrapped.
· in order to link a component with a renderer, you should know how to work with the UIComponent.setRenderType() method and with the component-family,component-type and renderer-type artifacts as level of annotations or with the <render-kit> and <renderer> tag in faces-config.xml. A detailed dissertation about them is the OmniFaces Custom Components Approach post. OmniFaces provides solid techniques for writing custom components and renderers!
How do I usually write a RenderKitFactory skeleton ?
· usually, you will extend the RenderKitFactory class, override the necessary methods, and configure it in the faces-config.xml via <render-kit-factory> tag
e.g. replace the default JSF RenderKit with the DummyRenderKit (source code)
DummyRenderKitFactory
public class DummyRenderKitFactory extends RenderKitFactory {
private static final Logger LOG = Logger.getLogger(DummyRenderKitFactory.class.getName());
private RenderKitFactory renderKitFactory;
public DummyRenderKitFactory() {
}
public DummyRenderKitFactory(RenderKitFactory renderKitFactory) {
this.renderKitFactory = renderKitFactory;
}
@Override
public void addRenderKit(String renderKitId, RenderKit renderKit) {
LOG.log(Level.INFO, "Register RenderKit with ID: {0}", renderKitId);
getWrapped().addRenderKit(renderKitId, renderKit);
}
@Override
public RenderKit getRenderKit(FacesContext context, String renderKitId) {
LOG.log(Level.INFO, "Delegate DummyRenderKit instead of RenderKit with ID: {0}", renderKitId);
RenderKit renderKit = getWrapped().getRenderKit(context, renderKitId);
return (HTML_BASIC_RENDER_KIT.equals(renderKitId)) ? new DummyRenderKit(renderKit) : renderKit;
}
@Override
public Iterator<String> getRenderKitIds() {
return getWrapped().getRenderKitIds();
}
@Override
public RenderKitFactory getWrapped() {
return renderKitFactory;
}
}
DummyRenderKit
public class DummyRenderKit extends RenderKitWrapper {
private static final Logger LOG = Logger.getLogger(DummyRenderKit.class.getName());
private RenderKit renderKit;
public DummyRenderKit() {
}
public DummyRenderKit(RenderKit renderKit) {
LOG.log(Level.INFO, "Default RenderKit instance provided by JSF: {0}", renderKit.getClass().getSimpleName());
this.renderKit = renderKit;
}
@Override
public Renderer getRenderer(String family, String rendererType) {
LOG.log(Level.INFO, "--- Delegating renderer of type {0} for component in family {1} ---", new Object[]{rendererType, family});
return getWrapped().getRenderer(family, rendererType);
}
@Override
public void addRenderer(String family, String rendererType, Renderer renderer) {
LOG.log(Level.INFO, "--- Adding the renderer of type {0} for component in family {1} ---", new Object[]{rendererType, family});
getWrapped().addRenderer(family, rendererType, renderer);
}
@Override
public RenderKit getWrapped() {
return renderKit;
}
}
faces-config.xml
...
<factory>
<render-kit-factory>
jsf.renderkitfactories.DummyRenderKitFactory
</render-kit-factory>
</factory>
...
Niciun comentariu :
Trimiteți un comentariu