3

The premise is quite straightforward: I have a page layout that relies on nested composite components (CC) to do the rendering, i.e. one JSF page references a CC, which itself references another CC, which contains a call to a third CC. - Source below.

Now, when that third CC wants to execute a bean method using ajax which would result in a fully self-contained partial update... nothing happens.*

Nested JSF CCs: Black: CC that executes a call from attributes. Red: Outer CC that passes its children to blue. Blue: Intermediate that inserts children for red.

I have searched SO and other places extensively, and have investigated all points of BalusC's insightful post here, but I've come up empty. Trace logging finally turned up the following message during the apply, validate and render stages, resulting in an "empty" response: FINER [javax.enterprise.resource.webcontainer.jsf.context] ... JSF1098: The following clientIds were not visited after a partial traversal: fooForm:j_idt14:j_idt15:j_idt18 . This is a waste of processor time and could be due to an error in the VDL page.

*) This only happens under very particular circumstances (which, though, are the exact definition of my use case):

  • Deeply nested CCs, at least two parent levels.
  • The nesting must be implicit, i.e. different CCs calling another, not just nested tags directly inside the calling page.
  • "Higher" CCs pass down children which are inserted in the "lower" CC using <cc:insertChildren />.
  • The CC that performs the ajax call and partial update contains actions dynamically created from the CC's attributes or clientId. (But not even necessarily in the same call, just inside the same component.)

All have to be met at the same time: If I use the innermost CC higher up in the hierarchy (or the nesting including the call to the final CC is all inside the calling page), everything works. If I use facets, no problem. If I remove or hard-code the action parameter, all good.

(Currently testing on EE6, EAP 6.4, but is the same in an EE7 project running on EAP 7.0)

Calling page:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:my="http://java.sun.com/jsf/composite/components/nestedcomponents">

    <h:head>
        <title>Nested composite component test</title>
    </h:head>

    <h:body>
        <h:form id="fooForm">           

        <h2>Works</h2>
        <my:randomString saveBean="#{util}" saveAction="doSomething" />


        <h2>Doesn't</h2>
        <my:containerInsertingAnotherUsingInsertChildren>
            <my:randomString saveBean="#{util}" saveAction="doSomething" />
        </my:containerInsertingAnotherUsingInsertChildren>

        </h:form>
    </h:body>
</html>

Innermost CC: (<my:randomString>, black frame; the #{util} is a request-scoped bean with one-liner dummy methods)

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

<cc:interface>
    <cc:attribute name="someValue" />
    <!--cc:attribute name="someAction" method-signature="void action()" /-->
    <!--cc:attribute name="someAction" targets="btn" targetAttributeName="action" /-->
    <cc:attribute name="saveBean" />
    <cc:attribute name="saveAction" />
</cc:interface>

<cc:implementation>
    <h:panelGroup layout="block" id="box" style="border: 1px solid black; margin: 3px; padding: 3px;">
        <h:outputText value="#{cc.attrs.id} / #{cc.clientId} / #{util.getRandomString()} " />

        <h:commandLink id="btn" value="save!" action="#{cc.attrs.saveBean[cc.attrs.saveAction]}" >
            <f:ajax render="box" immediate="true" />
        </h:commandLink>
    </h:panelGroup>
</cc:implementation>

</html>

Outer, wrapping CC: (<my:containerInsertingAnotherUsingInsertChildren>, red frame)

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:my="http://java.sun.com/jsf/composite/components/nestedcomponents">

<cc:interface>
</cc:interface>

<cc:implementation>
    <h:panelGroup layout="block" style="border: 1px solid red; margin: 3px; padding: 3px;">
        <my:containerUsingInsertChildren>
            <cc:insertChildren />
        </my:containerUsingInsertChildren>
    </h:panelGroup>
</cc:implementation>

</html>

Intermediate CC: (<my:containerUsingInsertChildren>, blue frame)

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui">

<cc:interface>
</cc:interface>

<cc:implementation>
    <h:panelGroup layout="block" style="border: 1px solid blue; margin: 3px; padding: 3px;">
        <cc:insertChildren />
    </h:panelGroup>
</cc:implementation>

</html>

As I wrote, hard-coded calls work as expected and update the little attached box. As soon as the bean method involves parameters (attributes) of the CC, and the CC sits deep enough in the hierarchy, they just get skipped.

I'm at a loss, and solutions or workarounds are most welcome.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Antares42
  • 1,406
  • 1
  • 15
  • 45

1 Answers1

3

This is caused by a bug in Mojarra related to generating the client ID of a composite component nested via <cc:insertChildren>. If you assign the composite components a fixed ID as in:

<h:form id="form">
    <my:level1 id="level1">
        <my:level3 id="level3" beanInstance="#{bean}" methodName="action" />
    </my:level1>
</h:form>

Whereby level1.xhtml is implemented as:

<cc:implementation>
    <my:level2 id="level2">
        <cc:insertChildren />
    </my:level2>
</cc:implementation>

And level2.xhtml as:

<cc:implementation>
    <cc:insertChildren />
</cc:implementation>

And level3.xhtml as:

<cc:implementation>
    <h:commandButton id="button" value="Submit #{component.clientId}"
        action="#{cc.attrs.beanInstance[cc.attrs.methodName]}">
        <f:ajax />
    </h:commandButton>
</cc:implementation>

Then you would notice that the #{component.clientId} in the submit button says form:level1:level3:button instead of form:level1:level2:level3:button as per the expectation (see also the answer to this related question How to find out client ID of component for ajax update/render? Cannot find component with expression "foo" referenced from "bar").

This was the clue to find the mistake in tree traversal. The log message which you got is actually misleading.

JSF1098: The following clientIds were not visited after a partial traversal: form:level1:level3:button. This is a waste of processor time and could be due to an error in the VDL page.

The first part is correct, that's indeed the technical problem, but the assumption about the potential cause as implied by "This is a waste of processor time and could be due to an error in the VDL page" is incorrect. It would theoretically only happen if the visit resulted in a stack overflow error, which happens only around 1000 levels deep. This is far from the case here.

Coming back to the root cause of the problem of the wrong client ID, this is unfortunately not trivial to work around without fixing the core JSF implementation itself (I've reported it as issue 4339). However, you can work around it by supplying a custom visit context which provides the correct sub tree IDs to visit. Here it is:

public class PartialVisitContextPatch extends VisitContextWrapper {
    private final VisitContext wrapped;
    private final Pattern separatorCharPattern;

    public PartialVisitContextPatch(VisitContext wrapped) {
        this.wrapped = wrapped;
        char separatorChar = UINamingContainer.getSeparatorChar(FacesContext.getCurrentInstance());
        separatorCharPattern = Pattern.compile(Pattern.quote(Character.toString(separatorChar)));
    }

    @Override
    public VisitContext getWrapped() {
        return wrapped;
    }

    @Override
    public Collection<String> getSubtreeIdsToVisit(UIComponent component) {
        Collection<String> subtreeIdsToVisit = super.getSubtreeIdsToVisit(component);

        if (subtreeIdsToVisit != VisitContext.ALL_IDS) {
            FacesContext context = getFacesContext();
            Map<String, Set<String>> cachedSubtreeIdsToVisit = (Map<String, Set<String>>) context.getAttributes()
                .computeIfAbsent(PartialVisitContextPatch.class.getName(), k -> new HashMap<String, Set<String>>());

            return cachedSubtreeIdsToVisit.computeIfAbsent(component.getClientId(context), k ->
                getIdsToVisit().stream()
                    .flatMap(id -> Arrays.stream(separatorCharPattern.split(id)))
                    .map(childId -> component.findComponent(childId))
                    .filter(Objects::nonNull)
                    .map(child -> child.getClientId(context))
                    .collect(Collectors.toSet())
            );
        }

        return subtreeIdsToVisit;
    }

    public static class Factory extends VisitContextFactory {
        private final VisitContextFactory wrapped;

        public Factory(VisitContextFactory wrapped) {
            this.wrapped = wrapped;
        }

        @Override
        public VisitContextFactory getWrapped() {
            return wrapped;
        }

        @Override
        public VisitContext getVisitContext(FacesContext context, Collection<String> ids, Set<VisitHint> hints) {
            return new PartialVisitContextPatch(getWrapped().getVisitContext(context, ids, hints));
        }
    }
}

By default, when Mojarra comes to <my:level2> during tree traversal and invokes getSubtreeIdsToVisit(), it would get an empty set, because the component ID string level2 is not present in the client ID string form:level1:level3:button. We need to override and manipulate getSubtreeIdsToVisit() in such way that it "correctly" returns form:level1:level3:button when <my:level2> is being passed in. This can be done by breaking down the client ID into parts form, level1, level3 and button and trying to find it as direct child of given component.

In order to get it to run, register it as below in faces-config.xml:

<factory>
    <visit-context-factory>com.example.PartialVisitContextPatch$Factory</visit-context-factory>
</factory>

Said that, make sure that you aren't abusing composite components for sake of templating. Better use tag files instead. See also When to use <ui:include>, tag files, composite components and/or custom components?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Sweet. Yeah, I dug through the source code and found where in ``PartialViewContextImpl`` the message is created, but had a time tracing where it actually neglects to visit parts of the tree. And indeed, based on your linked post, I had already refactored some of my components into tag files. As always, thanks for your insight and advice! – Antares42 Feb 26 '18 at 09:57