Search code examples
jsfjstlel

Programatically Set Attribute with a Value Expression when component is within JSTL forEach loop


First time asking a question so please bear with me. I have a pure Java custom component that extends UIInput (JSF 2.2, Mojarra) and I am using it like so:

<c:forEach items="#{bean.items}" var="item">
    <my:component item="#{item}" />
</c:forEach>

I am trying to avoid unnecessarily specifying the 'value', 'valueChangeListener' and 'validator' attributes on the tag in the .xhtml file.

In my custom component I have overridden the setValueExpression method like so:

@Override
public void setValueExpression(String name, ValueExpression expression) {
    super.setValueExpression(name, expression);
    if ("item".equals(name)) {
        this.setValue(Components.createValueExpression("#{item.myValue}", MyValue.class));
        this.addValueChangeListener(new MethodExpressionValueChangeListener(Components.createVoidMethodExpression("#{item.myValueChanged}", ValueChangeEvent.class)));
        this.addValidator(new MethodExpressionValidator(Components.createVoidMethodExpression("#{item.validateMyValue}", FacesContext.class, UIComponent.class, Object.class)));
    }
}

I'm using OmniFaces there with its Components utility to reduce boilerplate code.

When it comes time to act on any of those three (validate on submit, for example) it results in:

javax.faces.FacesException: javax.el.PropertyNotFoundException: Target Unreachable, identifier 'item' resolved to null

I am pretty sure I know why, I just don't know what to do about it.

I believe when it comes time for the three expressions I am trying to set programatically to be resolved it's trying to find a bean in some scope by the name, 'item' however that doesn't exist since 'item' was a point in time variable within a JSTL forEach loop.

I think there's a special deferred kind of expression that Weld is using for item itself (which I can kinda see when I debug that setValueExpression method) that is aware or otherwise has references to that point in time variable but I'm not doing the same thing when I set up those three expressions and therefore there's no handle to that later on when it comes time for them to be resolved.

I am sure there is a way to wire this together I'm just not seeing it.

Additionally, I know I could just put the three attributes on the tag in the .xhtml like this:

<my:component item="#{item}" value="#{item.myValue}" valueChangeListener="#{item.myValueChanged}" validator="#{item.validateMyValue}" />

Then they would get their special deferred expressions just like item itself does (and indeed everything works as expected this way) but I'd rather not - it's something I'd have to repeat a lot and it just seems like there should be a way to do what I am trying above.


Solution

  • I believe I found the answer.

    What I kept seeing in the debugger kept nagging at me, I just wasn't sure how to wire up the same scenario manually, then I found a Stack Overflow post with the following code snip at the very bottom:

    VariableMapper varMapper = new DefaultVariableMapper();
    varMapper.setVariable(mappingName, component.getValueExpression(mappedAttributeName));
    return new ValueExpressionImpl(expression, null, null, varMapper, expectedType);
    

    That was enough to point me in the right direction of re-using the incoming value expression for item itself in a VariableMapper instance which is in turn used to create the three value/method expressions so they each now have a handle & can resolve 'item' later on, when it comes time:

    @Override
    public void setValueExpression(String name, ValueExpression expression) {
        super.setValueExpression(name, expression);
        if ("item".equals(name)) {
    
            VariableMapper varMapper = new DefaultVariableMapper();
            varMapper.setVariable("item", expression);
    
            ValueExpressionImpl valExprImpl = new ValueExpressionImpl("#{item.myValue}", null, null, varMapper, MyValue.class);
            super.setValueExpression("value", valExprImpl);
    
            MethodExpressionImpl meExprImpl = new MethodExpressionImpl("#{item.myValueChanged}", null, null, varMapper, Void.class, new Class<?>[] {ValueChangeEvent.class});
            MethodExpressionValueChangeListener mevcl = new MethodExpressionValueChangeListener(meExprImpl);
            this.addValueChangeListener(mevcl);
    
            meExprImpl = new MethodExpressionImpl("#{item.validateMyValue}", null, null, varMapper, Void.class, new Class<?>[] {FacesContext.class, UIComponent.class, Object.class});
            MethodExpressionValidator mev = new MethodExpressionValidator(meExprImpl);
            this.addValidator(mev);
    
        }
    }
    

    That seems to have done the trick (and looks so simple now...).