JSF 复合组件目标操作在 c:forEach 标记内失败

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

我们在 c:forEach 标签内使用 commandButtons,其中按钮的 action 接收 forEach var 属性作为参数,如下所示:

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

这很好用。但是,如果我们将 commandButton 包装在复合组件中,它就不再起作用了:控制器被调用,但参数始终是 null。 下面是一个包含按钮和包装按钮的复合组件的 c:forEach 标记示例。第一个有效,第二个无效。

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

使用以下 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>

请注意,按钮的 value 属性(也绑定到 c:ForEach var)工作正常。只有通过复合组件的 targets 机制传播的 action 没有得到正确的评估。 Anoymone 能否解释一下,为什么会发生这种情况以及如何解决这个问题?

我们在 mojarra 2.2.8,el-api 2.2.5,tomcat 8.0.

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

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

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

Composite button:

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

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.

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.