Enterprise Java

Multiple dynamic includes with one JSF tag

Every JSF developer knows the ui:include and ui:param tags. You can include a facelet (XHTML file) and pass an object, which will be available in the included facelet, as follows:
 
 
 
 
 
 
 
 

<ui:include src="/sections/columns.xhtml">
    <ui:param name="columns" value="#{bean.columns}"/>
</ui:include>

So, you can e.g. use it within a PrimeFaces DataTable with dynamich columns (p:columns)

<p:dataTable value="#{bean.entries}" var="data" rowKey="#{data.id}" ...>
    ...
    <ui:include src="/sections/columns.xhtml">
        <ui:param name="data" value="#{data}"/>
        <ui:param name="columns" value="#{bean.columns}"/>
    </ui:include>

</p:dataTable>

where the included facelet could contain this code

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:p="http://primefaces.org/ui"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                ...>
    <p:columns value="#{columns}" var="column">
        <f:facet name="header">
            <h:outputText value="#{msgs[column.header]}"/>
        </f:facet>

        // place some input / select or complex composite component for multiple data types here.
        // a simple example for demonstration purpose:
        <p:inputText value="#{data[column.property]}"/>
    </p:columns>
</ui:composition>

#{bean.columns} refers to a List of special objects which describe the columns. I will name such objects ColumnModel. So, it is a
List <ColumnModel>. A ColumnModel has e.g. the attributes header and property.

Go on. Now, if we want to add a support for sorting / filtering, we can use dynamic paths which refer to specific facelet files containg sorting or / and filtering feature(s). Simple bind the src attribute to a bean property.

<ui:include src="#{bean.columnsIncludeSrc}">
   <ui:param name="data" value="#{data}"/>
   <ui:param name="columns" value="#{bean.columns}"/>
</ui:include>

The bean has something like

private boolean isFilterRight;
private boolean isSortRight

// setter / getter

public String getColumnsIncludeSrc() {
   if (isFilterRight && isSortRight) {
      return "/include/columnsTableFilterSort.xhtml";
   } else if (isFilterRight && !isSortRight) {
      return "/include/columnsTableFilter.xhtml";
   } else if (!isFilterRight && isSortRight) {
      return "/include/columnsTableSort.xhtml";
   } else {
      return "/include/columnsTable.xhtml";
   }
}

Different facelets are included dependent on the set boolean rights. So, the decision about what file to be included is placed within a bean. To be more flexible, we can encapsulate the table in a composite component and move the decision logic to the component class.

<cc:interface componentType="xxx.component.DataTable">
    <cc:attribute name="id" required="false" type="java.lang.String"
        shortDescription="Unique identifier of the component in a NamingContainer"/>
    <cc:attribute name="entries" required="true"
        shortDescription="The data which are shown in the datatable. This is a list of object representing one row."/>
    <cc:attribute name="columns" required="true" type="java.util.List"
        shortDescription="The columns which are shown in the datatable. This is a list of instances of type ColumnModel."/>
    ...
</cc:interface>
<cc:implementation>
    <p:dataTable value="#{cc.attrs.entries}" var="data" rowKey="#{data.id}" ...>
        ...
        <ui:include src="#{cc.columnsIncludeSrc}">
            <ui:param name="data" value="#{data}"/>
            <ui:param name="columns" value="#{cc.attrs.columns}"/>
        </ui:include>

    </p:dataTable>
</cc:implementation>

How does ui:include work? This is a tag handler which is applied when the view is being built. In JSF 2, the component tree is built twice on POST requests, once in RESTORE_VIEW phase and once in RENDER_RESPONSE phase. On GET it is built once in the RENDER_RESPONSE phase. This behavior is specified in the JSF 2 specification and is the same in Mojarra and MyFaces. The view building in the RENDER_RESPONSE is necessary in case the page author uses conditional includes or conditional templates. So, you can be sure that the src attribute of the ui:include gets evaluated shortly before the rendering phase.

But come to the point! What I wrote until now was an introduction for a motivation to extend ui:include. Recently, I got a task to use a p:dataTable with dynamic columns and p:rowEditor. Like this one in the PrimeFaces showcase. The problem is only – such editing feature doesn’t support p:columns. My idea was to add p:column tags multiple times, dynamically, but with different context parameters. You can imagine this as ui:include with ui:param in a loop. In the example above we intend to iterate over the List<ColumnModel>. Each loop iteration should make an instance of type ColumnModel available in the included facelet. So, I wrote a custom tag handler to include any facelet multiple times.

package xxx.taghandler;

import xxx.util.VariableMapperWrapper;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.component.UIComponent;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributeException;
import javax.faces.view.facelets.TagConfig;
import javax.faces.view.facelets.TagHandler;

/**
 * Tag handler to include a facelet multiple times with different contextes (objects from "value").
 * The attribute "value" can be either of type java.util.List or array.
 * If the "value" is null, the tag handler works as a standard ui:include.
 */
public class InlcudesTagHandler extends TagHandler {

    private final TagAttribute src;
    private final TagAttribute value;
    private final TagAttribute name;

    public InlcudesTagHandler(TagConfig config) {
        super(config);

        this.src = this.getRequiredAttribute("src");
        this.value = this.getAttribute("value");
        this.name = this.getAttribute("name");
    }

    @Override
    public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
        String path = this.src.getValue(ctx);
        if ((path == null) || (path.length() == 0)) {
            return;
        }

        // wrap the original mapper - this is important when some objects passed into include via ui:param
        // because ui:param invokes setVariable(...) on the set variable mappper instance
        VariableMapper origVarMapper = ctx.getVariableMapper();
        ctx.setVariableMapper(new VariableMapperWrapper(origVarMapper));

        try {
            this.nextHandler.apply(ctx, null);

            ValueExpression ve = (this.value != null) ? this.value.getValueExpression(ctx, Object.class) : null;
            Object objValue = (ve != null) ? ve.getValue(ctx) : null;

            if (objValue == null) {
                // include facelet only once
                ctx.includeFacelet(parent, path);
            } else {
                int size = 0;

                if (objValue instanceof List) {
                    size = ((List) objValue).size();
                } else if (objValue.getClass().isArray()) {
                    size = ((Object[]) objValue).length;
                }

                final ExpressionFactory exprFactory = ctx.getFacesContext().getApplication().getExpressionFactory();
                final String strName = this.name.getValue(ctx);

                // generate unique Id as a valid Java identifier and use it as variable for the provided value expression
                final String uniqueId = "a" + UUID.randomUUID().toString().replaceAll("-", "");
                ctx.getVariableMapper().setVariable(uniqueId, ve);

                // include facelet multiple times
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < size; i++) {
                    if ((strName != null) && (strName.length() != 0)) {
                        // create a new value expression in the array notation and bind it to the variable "name"
                        sb.append("#{");
                        sb.append(uniqueId);
                        sb.append("[");
                        sb.append(i);
                        sb.append("]}");

                        ctx.getVariableMapper().setVariable(strName,
                            exprFactory.createValueExpression(ctx, sb.toString(), Object.class));
                    }

                    // included facelet can access the created above value expression
                    ctx.includeFacelet(parent, path);

                    // reset for next iteration
                    sb.setLength(0);
                }
            }
        } catch (IOException e) {
            throw new TagAttributeException(this.tag, this.src, "Invalid path : " + path);
        } finally {
            // restore original mapper
            ctx.setVariableMapper(origVarMapper);
        }
    }
}

The most important call is ctx.includeFacelet(parent, path). The method includeFacelet(…) from the JSF API includes the facelet markup at some path relative to the current markup. The class VariableMapperWrapper is used for a name to value mapping via ui:param. For the example with columns the variable column will be mapped to expressions #{columns[0]}, #{columns[1]}, etc. before every includeFacelet(…) call as well. Well, not exactly to these expressions, at place of columns should be an unique name mapped again to the columns object (to avoid possible name collisions). The mapper class looks like as follows

package xxx.util;

import java.util.HashMap;
import java.util.Map;
import javax.el.ELException;
import javax.el.ValueExpression;
import javax.el.VariableMapper;

/**
 * Utility class for wrapping a VariableMapper. Modifications occur to the internal Map instance.
 * The resolving occurs first against the internal Map instance and then against the wrapped VariableMapper
 * if the Map doesn't contain the requested ValueExpression.
 */
public class VariableMapperWrapper extends VariableMapper {

    private final VariableMapper wrapped;

    private Map<String, ValueExpression> vars;

    public VariableMapperWrapper(VariableMapper orig) {
        super();
        this.wrapped = orig;
    }

    @Override
    public ValueExpression resolveVariable(String variable) {
        ValueExpression ve = null;
        try {
            if (this.vars != null) {
                // try to resolve against the internal map
                ve = this.vars.get(variable);
            }

            if (ve == null) {
                // look in the wrapped variable mapper
                return this.wrapped.resolveVariable(variable);
            }

            return ve;
        } catch (Throwable e) {
            throw new ELException("Could not resolve variable: " + variable, e);
        }
    }

    @Override
    public ValueExpression setVariable(String variable, ValueExpression expression) {
        if (this.vars == null) {
            this.vars = new HashMap<String, ValueExpression>();
        }

        return this.vars.put(variable, expression);
    }
}

Register the tag handler in a taglib XML file and you are done.

<tag>
    <tag-name>includes</tag-name>
    <handler-class>xxx.taghandler.InlcudesTagHandler</handler-class>
    <attribute>
        <description>
            <![CDATA[The relative path to a XHTML file to be include one or multiple times.]]>
        </description>
        <name>src</name>
        <required>true</required>
        <type>java.lang.String</type>
    </attribute>
    <attribute>
        <description>
            <![CDATA[Objects which should be available in the included XHTML files. This attribute can be either
            of type java.util.List or array. If it is null, the tag handler works as a standard ui:include.]]>
        </description>
        <name>value</name>
        <required>false</required>
        <type>java.lang.Object</type>
    </attribute>
    <attribute>
        <description>
            <![CDATA[The name of the parameter which points to an object of each iteration over the given value.]]>
        </description>
        <name>name</name>
        <required>false</required>
        <type>java.lang.String</type>
    </attribute>
</tag>

Now I was able to use it in a composite component as

<p:dataTable value="#{cc.attrs.entries}" var="data" rowKey="#{data.id}" ...>
    ...
    <custom:includes src="#{cc.columnsIncludeSrc}" value="#{cc.attrs.columns}" name="column">
        <ui:param name="data" value="#{data}"/>
    </custom:includes> 

</p:dataTable>

A typically facelet file (and the component tree) contains a quite regular p:column tag which means we are able to use all DataTable’s features!

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:p="http://primefaces.org/ui"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                ...>
    <p:column headerText="#{msgs[column.header]}">
        <p:cellEditor>
            <f:facet name="output">
                <custom:typedOutput outputType="#{column.outputTypeName}"
                   typedData="#{column.typedData}"
                   value="#{data[column.property]}"
                   timeZone="#{cc.timeZone}"
                   calendarPattern="#{cc.calendarPattern}"       
                   locale="#{cc.locale}"/>
            </f:facet>

            <f:facet name="input">
                <custom:typedInput inputType="#{column.inputTypeName}"
                   typedData="#{column.typedData}"
                   label="#{column.inputTypeName}"
                   value="#{data[column.property]}"
                   onchange="highlightEditedRow(this)"
                   timeZone="#{cc.timeZone}"
                   calendarPattern="#{cc.calendarPattern}"
                   locale="#{cc.locale}"/>
            </f:facet>
        </p:cellEditor>
    </p:column>
</ui:composition>

Note: This approach can be applied to another components and use cases. The InlcudesTagHandler works universell. For instance, I can imagine to create a dynamic Menu component in PrimeFaces without an underlying MenuModel. A list or array of some model class is still needed of course.
 

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Manivannan
Manivannan
9 years ago

I have a similar kind of requirement, Can I have Complete code for this example ?

Back to top button