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.

+3


source to share


1 answer


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

.



enter image description here 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

:

Regular button: enter image description here

We can see that the regular button uses DefaultFaceletContext

how ELContext

and what VariableMapper

contains the right variable item

.

Composite Button:

enter image description here 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

.

enter image description here

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.

enter image description here

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

.

+1


source







All Articles