JSF Composite Component target actions fail in c: forEach tag
We use commandButtons
inside a tag c:forEach
where the button action
receives an attribute forEach
var
as a parameter like this:
<c:forEach var="myItem" items="#{myModel}">
<h:commandButton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
</c:forEach>
This works great. If we wrap commandButton
in a composite component, this no longer works: the controller is called, but the parameter is always null
. Below is an example of a tag c:forEach
containing a button and a composite component that wraps the button. The first one works, the second one doesn't.
<c:forEach var="myItem" items="#{myModel}">
<h:commandButton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
<my:mybutton
action="#{myController.process(myItem)}"
value="#{myItem.name}" />
</c:forEach>
with the following implementation my:mybutton
:
<composite:interface>
<composite:attribute name="action" required="true" targets="button" />
<composite:attribute name="value" required="true" />
</composite:interface>
<composite:implementation>
<h:commandButton id="button"
value="#{cc.attrs.value}">
</h:commandButton>
</composite:implementation>
Note that the value
button attribute , which is also bound to c:forEach
var
, works fine. It is only action
propagated through the composite component mechanism targets
and is not being evaluated correctly. Can anoymone explain why this is happening and how to fix it?
We are on mojarra 2.2.8, el-api 2.2.5, tomcat 8.0.
source to share
This is caused by a bug in Mojarra, or perhaps an oversight in the JSF specification regarding retargeting method expressions for composite components.
Work around below ViewDeclarationLanguage
.
public class FaceletViewHandlingStrategyPatch extends ViewDeclarationLanguageFactory {
private ViewDeclarationLanguageFactory wrapped;
public FaceletViewHandlingStrategyPatch(ViewDeclarationLanguageFactory wrapped) {
this.wrapped = wrapped;
}
@Override
public ViewDeclarationLanguage getViewDeclarationLanguage(String viewId) {
return new FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch(getWrapped().getViewDeclarationLanguage(viewId));
}
@Override
public ViewDeclarationLanguageFactory getWrapped() {
return wrapped;
}
private class FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch extends ViewDeclarationLanguageWrapper {
private ViewDeclarationLanguage wrapped;
public FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch(ViewDeclarationLanguage wrapped) {
this.wrapped = wrapped;
}
@Override
public void retargetMethodExpressions(FacesContext context, UIComponent topLevelComponent) {
super.retargetMethodExpressions(new FacesContextWithFaceletContextAsELContext(context), topLevelComponent);
}
@Override
public ViewDeclarationLanguage getWrapped() {
return wrapped;
}
}
private class FacesContextWithFaceletContextAsELContext extends FacesContextWrapper {
private FacesContext wrapped;
public FacesContextWithFaceletContextAsELContext(FacesContext wrapped) {
this.wrapped = wrapped;
}
@Override
public ELContext getELContext() {
boolean isViewBuildTime = TRUE.equals(getWrapped().getAttributes().get(IS_BUILDING_INITIAL_STATE));
FaceletContext faceletContext = (FaceletContext) getWrapped().getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
return (isViewBuildTime && faceletContext != null) ? faceletContext : super.getELContext();
}
@Override
public FacesContext getWrapped() {
return wrapped;
}
}
}
Install it as shown below in faces-config.xml
:
<factory>
<view-declaration-language-factory>com.example.FaceletViewHandlingStrategyPatch</view-declaration-language-factory>
</factory>
How did I nail it?
We confirmed that the problem is that the method expression argument becomes null
when an action is called in a composite component and the action itself is declared in another composite component.
<h:form>
<my:forEachComposite items="#{['one', 'two', 'three']}" />
</h:form>
<cc:interface>
<cc:attribute name="items" required="true" />
</cc:interface>
<cc:implementation>
<c:forEach items="#{cc.attrs.items}" var="item">
<h:commandButton id="regularButton" value="regular button" action="#{bean.action(item)}" />
<my:buttonComposite value="cc button" action="#{bean.action(item)}" />
</c:forEach>
</cc:implementation>
<cc:interface>
<cc:attribute name="action" required="true" targets="compositeButton" />
<cc:actionSource name=""></cc:actionSource>
<cc:attribute name="value" required="true" />
</cc:interface>
<cc:implementation>
<h:commandButton id="compositeButton" value="#{cc.attrs.value}" />
</cc:implementation>
The first thing I did was find the code responsible for MethodExpression
instantiating the #{bean.action(item)}
. I know it is usually created via ExpressionFactory#createMethodExpression()
. I also know that all EL context variables are usually exposed via ELContext#getVariableMapper()
. So I put a debug breakpoint in createMethodExpression()
.
In the call stack, we can check ELContext#getVariableMapper()
as well as the creator MethodExpression
. On a test page with a composite component nested in one regular command button and one complex command button, we can see the following difference in ELContext
:
We can see that the regular button uses DefaultFaceletContext
how ELContext
and what VariableMapper
contains the right variable item
.
Composite Button:
We can see that the composite button uses the standard ELContextImpl
like ELContext
and what VariableMapper
does not contain the right variable item
. Therefore, we need to take a few steps back in the call stack to find out where this standard comes from ELContextImpl
and why it is being used instead DefaultFaceletContext
.
Once a specific implementation is found ELContext
, we can find what it is derived from FacesContext#getElContext()
. But this is not the EL context of the composite component! This is represented by the current value FaceletContext
. Therefore, we need to take a few more steps to understand why FaceletContext
it is not transmitted.
Here we can see what CompositeComponentTagHandler#applyNextHander()
does not go through FaceletContext
as well FacesContext
. This is the exact part that may have been an oversight in the JSF spec. ViewDeclarationLanguage#retargetMethodExpressions()
should have given another argument representing valid ELContext
.
But that's what it is. Right now, we can't change the spec on the fly. The best we can do is report the problem to them.
The above value FaceletViewHandlingStrategyPatch
works as follows, ultimately overriding FacesContext#getElContext()
as shown below:
@Override
public ELContext getELContext() {
boolean isViewBuildTime = TRUE.equals(getWrapped().getAttributes().get(IS_BUILDING_INITIAL_STATE));
FaceletContext faceletContext = (FaceletContext) getWrapped().getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
return (isViewBuildTime && faceletContext != null) ? faceletContext : super.getELContext();
}
You see, it checks if JSF is currently creating the view and is present FaceletContext
. If so, return FaceletContext
instead of the standard implementation ELContext
(note that it FaceletContext
just extendsELContext
). Thus, it MethodExpression
will be created with the correct ELContext
one holding the right one VariableMapper
.
source to share