Search code examples
jsfjstlelcomposite-componentmojarra

JSF Composite Component target actions fail within the c:forEach Tag


We use commandButtons inside a c:forEach tag, where the button's action receives the forEach var attribute as a parameter like this:

<c:forEach var="myItem" items="#{myModel}">
    <h:commandButton
        action="#{myController.process(myItem)}"
        value="#{myItem.name}" />
</c:forEach>

This works just fine. If we wrap the commandButton in a composite component, it does not work anymore, though: The controller gets called, but parameter is always null. Here is an example of a c:forEach Tag containing a button and composite component wrapping a button. The first one works, the second one does not.

<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 my:mybutton implementation:

<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>

Please Note, that the button's value attribute, which is also bound to the c:ForEach var, works just fine. It is only the action, propagated through the composite component's targets mechanism, that does not get evaluated correctly. Can anoymone please explain, why this happens and how to fix this?

We are on mojarra 2.2.8, el-api 2.2.5, tomcat 8.0.


Solution

  • This is caused by a bug in Mojarra or perhaps an oversight in the JSF specification with regard to retargeting method expressions for composite components.

    The work around is the 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 below in faces-config.xml:

    <factory>
        <view-declaration-language-factory>com.example.FaceletViewHandlingStrategyPatch</view-declaration-language-factory>
    </factory>
    

    How I nailed down it?

    We have confirmed that the problem is that the method expression argument became null when the action is invoked in a composite component while 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>
    

    First thing I did was locating the code who's responsible for creating the MethodExpression instance behind #{bean.action(item)}. I know it's normally created via ExpressionFactory#createMethodExpression(). I also know that all EL context variables are normally provided via ELContext#getVariableMapper(). So I placed a debug breakpoint in createMethodExpression().

    enter image description here ​ In the call stack we can inspect the ELContext#getVariableMapper() and also who's responsible for creating the MethodExpression. In a test page with a composite component in turn nesting one regular command button and one composite command button we can see the following difference in ELContext:

    Regular button: ​ enter image description here

    We can see that the regular button uses DefaultFaceletContext as ELContext and that the VariableMapper contains the right item variable.

    Composite button:

    enter image description here ​​ We can see that the composite button uses standard ELContextImpl as ELContext and that the VariableMapper doesn't contain the right item variable. So we need to go some steps back in the call stack to see where this standard ELContextImpl is coming from and why it's being used instead of DefaultFaceletContext.

    enter image description here

    Once found where the particular ELContext implementation is created, we can find that it's obtained from FacesContext#getElContext(). But this does not represent the EL context of the composite component! This is represented by the current FaceletContext. So we need to go some more steps back to figure out why the FaceletContext isn't being passed down.

    enter image description here

    We can see here that CompositeComponentTagHandler#applyNextHander() doesn't pass through the FaceletContext but the FacesContext instead. This is the exact part which might have been an oversight in the JSF specification. The ViewDeclarationLanguage#retargetMethodExpressions() should have asked for another argument, representing the actual ELContext involved.

    But it is what it is. We can't change the spec on the fly right now. Best what we can do is to report an issue to them.

    The above shown FaceletViewHandlingStrategyPatch works as follows by ultimately overriding the FacesContext#getELContext() as 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 building the view and if there's a FaceletContext present. If so, then return the FaceletContext instead of the standard ELContext implementation (note that FaceletContext just extends ELContext). This way the MethodExpression will be created with the right ELContext holding the right VariableMapper.