JSF 2.2 ViewDeclarationLanguage createComponent passes attributes as String?

352 views Asked by At

I am trying to write a JSF custom component that dynamically chooses creates and renders an existing composite component. So far everthing is working fine except for passing attributes to the composite.

This is my custom component class (error handling etc. stripped for better reading):

@FacesComponent(createTag = true)
public class ChooseEZComp extends UIComponentBase {

  @Override
  public void encodeBegin(FacesContext context) throws IOException {
    Object value = getAttributes().get("value");

    String ezCompName = value.getClass().getSimpleName().toLowerCase();
    // ezCompName is something like "freelink" or "treenode"

    Map<String, Object> params = new HashMap<>();
    params.put("node", value);
    // log.debug(params.get("node").getClass().getName()) -> yields correct class name

    ViewDeclarationLanguage viewDeclarationLanguage = context
      .getApplication()
      .getViewHandler()
      .getViewDeclarationLanguage(context, context.getViewRoot().getViewId());

    UIComponent component = viewDeclarationLanguage
      .createComponent(context,
        "http://xmlns.jcp.org/jsf/composite/ezcomp",
        ezCompName,
        params);

    component.encodeAll(context);
  }
}

A composite component (I have several of them), that gets choosen and rendered by this class:

  <cc:interface>
    <cc:attribute name="node" required="true"/>
  </cc:interface>

  <cc:implementation>
    <h:outputText value="The class of node is: #{cc.attrs.node.class.name}"/>
  </cc:implementation>

This is how I use the tag in my JSF page:

<test:chooseEZComp value="#{treeView.selectedNode.data}"/>

So "value" is always guaranteed not of type java.lang.String (it's some JPA @Entity).

But nevertheless the result output in the JSF page is always:

The class of node is: java.lang.String

Where am I wrong? Isn't it possible to pass something other than String as parameter to an composite?

I am runnind wildfly-8.2.0-final with Java EE 7 (and Primefaces 5 but which is not used here)

Any hints welcome!


Edit: of course I also tried to force the type of the attribute in the cc:interface

<cc:interface>
  <cc:attribute name="node" required="true" type="some.package.type"/>
</cc:interface>

But this consequently resulted in a IllegalArgument Exception:

IllegalArgumentException: Cannot convert ... of type class java.lang.String to class

1

There are 1 answers

0
Lasrik On BEST ANSWER

Turns out I misunderstood the API ... Map<String, Object> in the signature made me think I can pass an object. But the Javadoc is more precise about this:

attributes - any name=value pairs that would otherwise have been given on the markup that would cause the creation of this component [..]

So you do not pass the value into createComponent but rather the expression that was used to calculate the value for the specified attribute or property name:

  Map<String, Object> attributes = new HashMap<>();
  ValueExpression valueExpression = getValueExpression("value");
  attributes.put("node", valueExpression.getExpressionString());

Funny side read: to find a solution I debugged through the jsf-imp-2.2.8-jbossorg and stumbled upon the code to create the component. Basically what it does is:

  • create a JSF xhtml file in the temp folder and use OutputStreamWriter#append to write a JSF page with exactly one tag in it (the one you want to create)
  • loop through all attributes and write them as attribues into the tag
  • Save the file and feed it to the DefaultFaceletFactory#createFacelet
  • create a naming container and make it parent of the generated facelet (apply)
  • use findComponent on the naming container to get hold of the generated tag and return it

At least after finding this it's clear why you need to pass in the value expression rather than the value itself.