Hi
In JSF 2.2, it was added a new method called VDL.createComponent(...) .
The intention was "normalize" programatic additions of composite components
to a view or component tree. But over the time, I have notice some suggestions
that deserves the attention of the EG. Additionally, I have found some cases
that does not work as it should be with the reference implementation (Mojarra
2.2.6 and earlier versions), and that raise some concerns about the
portability of this feature between implementations of the same JSF spec.
This mail continues the following discussion on this list:
[jsr344-experts] implications of use FaceletFactory
(JAVASERVERFACES_SPEC_PUBLIC-611)
https://java.net/projects/javaserverfaces-spec-public/lists/jsr344-experts/archive/2012-11/message/91
The related issue in MyFaces is here:
https://issues.apache.org/jira/browse/MYFACES-3733
There are three possible options to use VDL.createComponent(...):
// Normal component
UIComponent component = vdl.createComponent(facesContext,
"
http://java.sun.com/jsf/html",
"outputText", attributes);
// Composite component
UIComponent component = vdl.createComponent(facesContext,
"
http://java.sun.com/jsf/composite/testComposite",
"dynComp_1", attributes);
// Dynamic <ui:include >
Map<String, Object> attributes = new HashMap<String, Object>();
attributes.put("src", "/addSimpleIncludeVDL_1_1.xhtml");
UIComponent component = vdl.createComponent(facesContext,
"
http://java.sun.com/jsf/facelets",
"include", attributes);
The first two are pretty obvious that should work, but the third one is tricky.
Should <ui:include ...> tag be considered a component? From a technical
perspective, ui:include is a facelet template tag, so there is no underlying
component behind it. But an user could easily think the opposite. He/She could
say "... an ui:include is just a component that should put the content of the
fragment point by "src" attribute ...".
The syntax is just straightforward. The method returns a component that you
can attach to the component tree in the place you want. The logic behind the
algorithm should hide details like if multiple components are returned from
the ui:include call, just wrap them into a single panel and so on. The
invocation should be legal from spec perspective, even if it was not devised
in the moment the feature was conceived, so why don't support it?.
Now the problem.
Some time ago, I did some black box tests over Mojarra to see what happen in
these cases, but I found out that the feature is not working as it should.
Let's take a look at this example:
@FacesComponent(value = "com.myapp.UIAddSimpleCCVDL")
public class UIAddSimpleCCVDL extends UIComponentBase implements
SystemEventListener
{
public UIAddSimpleCCVDL()
{
setRendererType(null);
FacesContext context = FacesContext.getCurrentInstance();
UIViewRoot root = context.getViewRoot();
root.subscribeToViewEvent(PreRenderViewEvent.class, this);
}
/* ... some code not related to the test... */
public void processEvent(SystemEvent event) throws AbortProcessingException
{
FacesContext facesContext = FacesContext.getCurrentInstance();
if (!facesContext.isPostback())
{
ViewDeclarationLanguage vdl = facesContext.getApplication().
getViewHandler().getViewDeclarationLanguage(
facesContext, facesContext.getViewRoot().getViewId());
Map<String, Object> attributes = new HashMap<String, Object>();
UIComponent component = vdl.createComponent(facesContext,
"
http://java.sun.com/jsf/composite/testComposite",
"dynComp_1", attributes);
getChildren().add(component);
}
}
}
/resources/testComposite/dynComp_1.xhtml
<cc:interface>
</cc:interface>
<cc:implementation>
Dynamically added markup
<h:outputText value="Dynamically added child"/>
</cc:implementation>
/addSimpleCCVDL.xhtml (the page that declares it)
<test:addSimpleCCVDL id="component"/>
/WEB-INF/testcomposite.taglib.xml
<facelet-taglib>
<namespace>
http://testcomponent</namespace>
<tag>
<tag-name>addSimpleCCVDL</tag-name>
<component>
<component-type>com.myapp.UIAddSimpleCCVDL</component-type>
</component>
</tag>
<!-- ... -->
</facelet-taglib>
The example is just a normal component that use a listener subscribed to
PreRenderViewEvent and add the composite component to the component tree.
This is a typical case and things should work.
But take a look at the implementation. There is a real JSF component
(h:outputText) and some HTML markup. Facelets use a wrapper component
called UILeaf that wraps the HTML markup but please note this component
is a "transient" component, which means this component does not have
any state to be saved, so it is not saved in the state. When the component
tree is restored, the HTML markup should be "refreshed" somehow to
reconstruct the component tree the way it was before render response.
The problem in Mojarra is, the code that should do the refresh is not there.
Just put a button and do a postback over the page and you'll see the
HTML markup just vanish, but the h:outputText component do not, because
it is a component that was included with the state.
I will not bore you with the implementation details required to make it run.
Instead, I will just say that a solution requires to use something (like a
listener for example) to refresh the tree when it is restored.
Now let's suppose that some fix was done and now let's try to modify
the example just a bit:
/resources/testComposite/dynComp_1.xhtml
<cc:interface>
<cc:facet name="header" required="true"/>
</cc:interface>
<cc:implementation>
<h:panelGrid>
<cc:insertFacet name="header"/>
</h:panelGrid>
Dynamically added markup
<h:outputText value="Dynamically added child"/>
</cc:implementation>
public void processEvent(SystemEvent event) throws AbortProcessingException
{
FacesContext facesContext = FacesContext.getCurrentInstance();
if (!facesContext.isPostback())
{
ViewDeclarationLanguage vdl = facesContext.getApplication().
getViewHandler().getViewDeclarationLanguage(
facesContext, facesContext.getViewRoot().getViewId());
Map<String, Object> attributes = new HashMap<String, Object>();
UIComponent component = vdl.createComponent(facesContext,
"
http://java.sun.com/jsf/composite/testComposite",
"dynComp_1", attributes);
UIOutput text = (UIOutput) facesContext.getApplication().
createComponent(UIOutput.COMPONENT_TYPE);
text.setValue("Dynamically added header");
component.getFacets().put("header", text);
getChildren().add(component);
}
}
In this example, the facet is added to the composite component, and then the
component is added to the component tree. In Mojarra, the example does not
work, because in some point it tries to check if the facet is there but
it is not. Here is the stack trace.
javax.faces.view.facelets.TagException:
//G:/workspace/myfaces/test22/myfaces-helloworld22-test-comp-prog/target/tmp/mojarra6162439279887177630.tmp
@2,102 <j:dynComp_1> The following facets(s) are required, but no
facets have been supplied for them: header.
at com.sun.faces.facelets.tag.composite.InterfaceHandler.validateComponent(InterfaceHandler.java:232)
at com.sun.faces.facelets.tag.composite.InterfaceHandler.apply(InterfaceHandler.java:125)
at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:312)
at com.sun.faces.facelets.impl.DefaultFacelet.include(DefaultFacelet.java:371)
at com.sun.faces.facelets.impl.DefaultFaceletContext.includeFacelet(DefaultFaceletContext.java:326)
at com.sun.faces.facelets.tag.jsf.CompositeComponentTagHandler.applyCompositeComponent(CompositeComponentTagHandler.java:385)
at com.sun.faces.facelets.tag.jsf.CompositeComponentTagHandler.applyNextHandler(CompositeComponentTagHandler.java:186)
at com.sun.faces.facelets.tag.jsf.ComponentTagHandlerDelegateImpl.apply(ComponentTagHandlerDelegateImpl.java:203)
at javax.faces.view.facelets.DelegatingMetaTagHandler.apply(DelegatingMetaTagHandler.java:120)
at javax.faces.view.facelets.CompositeFaceletHandler.apply(CompositeFaceletHandler.java:95)
at com.sun.faces.facelets.compiler.NamespaceHandler.apply(NamespaceHandler.java:93)
at com.sun.faces.facelets.compiler.EncodingHandler.apply(EncodingHandler.java:87)
at com.sun.faces.facelets.impl.DefaultFacelet.apply(DefaultFacelet.java:161)
at com.sun.faces.facelets.impl.DefaultFaceletFactory._createComponent(DefaultFaceletFactory.java:403)
at com.sun.faces.application.view.FaceletViewHandlingStrategy.createComponent(FaceletViewHandlingStrategy.java:841)
at org.apache.myfaces.examples.helloworld22.UIAddSimpleCCVDL.processEvent(UIAddSimpleCCVDL.java:81)
The problem is the call to VDL.createComponent cause the composite component to
be applied, but it is too early for that, because it is necessary to let the
user time to put the facet and then trigger the composite component code
when the component is added to the component tree. But if you remove the
required="true" from the facet, the code "apparently" works. Anyway the stack
trace looks like something that is not working as it should.
Now let's try this example (it uses the same composite component):
/componentBindingVDL_1.xhtml
<h:panelGroup id="panel" binding="#{componentBindingVDLBean1.panel}"/>
<h:form id="mainForm">
<h:commandButton id="postback" value="POSTBACK"/>
</h:form>
@ManagedBean(name="componentBindingVDLBean1")
@RequestScoped
public class ComponentBindingVDLBean1
{
private UIPanel panel;
public UIPanel getPanel()
{
if (panel == null)
{
panel = new HtmlPanelGroup();
FacesContext facesContext = FacesContext.getCurrentInstance();
ViewDeclarationLanguage vdl = facesContext.getApplication().
getViewHandler().getViewDeclarationLanguage(
facesContext, facesContext.getViewRoot().getViewId());
Map<String, Object> attributes = new HashMap<String, Object>();
UIComponent cc = vdl.createComponent(facesContext,
"
http://java.sun.com/jsf/composite/testComposite",
"dynComp_1", attributes);
UIOutput text = (UIOutput) facesContext.getApplication().
createComponent(UIOutput.COMPONENT_TYPE);
text.setValue("Dynamically added header");
cc.getFacets().put("header", text);
panel.getChildren().add(cc);
}
return panel;
}
This is like the previous one but please note the component is injected into
the tree through the "binding" attribute. This one also does not work with
Mojarra, but in this case it is much worst, because not only the html markup
but the whole composite component disappear. This case is quite special
because the h:panelGroup is managed by facelets but the content is created
by the user, but note the composite component needs to be refreshed by
facelets again. Think about what will happen with a <c:if ...> tag inside
the composite component.
These problems are only the tip of the Iceberg. It is possible to find more
problems, but at the end, the important thing is to get to an agreement about
which cases should work and which ones should not. I have to say that all
previous cases works with MyFaces 2.2, and according to the spec, all
these cases should work like magic. The javadoc of VDL.createComponent(...)
says this:
"... Create a component given a ViewDeclarationLanguage specific tag
library URI and tag name. The runtime must support this method operating
for the Facelets VDL. ..."
In my personal opinion, and based on the work done with MyFaces, at least
these 3 minimal cases should be valid for JSF 2.2. Enable ui:include is
also wanted, since many users are looking for a solution to avoid ugly
and error prone hacks related to FaceletFactory.
My suggestion is if the code in Mojarra requires some refactoring, please
be sure to include the previous cases on the fix, so both implementations
point in the same direction.
regards,
Leonardo Uribe