1

I'm using JSF 2.2 / Mojarra 2.2.8

In my model there are java.util.Set and I want to edit those Set

public class MyModel {
    private Set<Foo> fooSet;
    private Set<Bar> barSet;
    // getters and setters
}

public class Foo {
    private String label;
    //getter and setter
}

public class Bar {
    private String name;
    // getter and setter
}

I'm using composite component for that

<h:form>
    <ez:editFooSet myModel="#{someBean.myModel}"/>
    <ez:editBarSet myModel="#{someBean.myModel}"/>
    <!-- ... -->
</h:form>

My idea is to store a List needed by the ui:repeat in a JSF ManagedBean and use a @FacesComponent to convert the Set to List in encodeBegin() and the List to Set in updateModel()

editFooSet.xhtml :

<cc:interface componentType="my.app.component.FooSetComponent">
    <cc:attribute name="myModel" type="my.app.model.MyModel" required="true"/>
</cc:interface>
<cc:implementation>
    <ui:repeat value="#{fooSetBean.value}" var="item">
        <h:outputLabel value="Foo label: "/>
        <h:inputText value="#{item.label}"/>
        <h:commandButton value="remove" action="#{fooSetBean.remove(item)}"/>
    </ui:repeat>
    <h:commandButton value="add" action="#{fooSetBean.add()}"/>
</cc:implementation>

FooSetBean.java

@Named
@ViewScoped
public class FooSetBean {
    private List<Foo> value;
    // getter and setter
    puvlic void remove(Foo foo) {
        fooList.remove(foo);
    }
    public void add() {
        fooList.add(new Foo());
    }
}

and the FooSetComponent.java :

@FacesComponent("my.app.component.FooSetComponent")
public class FooSetComponent extends UIInput implements NamingContainer {

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public Object getSubmittedValue() {
        return null;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        MyModel model = (MyModel) super.getAttributes().get("myModel");
        Collection<Foo> foos = model.getFooSet();
        List<Foo> fooList = new ArrayList<>(foos);
        FooSetBean bean = context.getApplication().evaluateExpressionGet(context, "#{fooSetBean}", FooSetBean.class) ;
        bean.setValue(fooList);
        super.encodeBegin(context);
    }

    @Override
    public void updateModel(FacesContext context) {
        MyModel model = (MyModel) super.getAttributes().get("myModel");
        FooSetBean bean = context.getApplication().evaluateExpressionGet(context, "#{fooSetBean}", FooSetBean.class) ;
        Collection<Foo> newValue = bean.getValue();
        model.setFooSet(new HashSet<>(newValue));
    }
}

and the same for editBarSet.xhtml, BarSetBean.java and BarSetComponent.java

And that solution is working

My problem is that I have a lot of those Set and I want to factorize this code

I want to have something like that :

<h:form>
    <ez:editRepeat value="#{someBean.myModel.fooSet}" itemClass="#{Foo.class}">
        <h:outputLabel value="Foo label: "/>
        <h:inputText value="#{item.label"/>
    </ez:editRepeat>
    <ez:editRepeat value="#{someBean.myModel.barSet}" itemClass="#{Bar.class}">
        <h:outputLabel value="Bar name: "/>
        <h:inputText value="#{item.name}"/>
    </ez:editRepeat>
    <!-- ... -->
</h:form>

with the editRepeat.xhtml :

<cc:interface componentType="my.app.component.EditRepeatComponent">
    <cc:attribute name="value" type="java.util.collection" required="true"/>
    <cc:attribute name="itemClass" type="java.lang.Class" required="true"/>
</cc:interface>
<cc:implementation>
    <ui:repeat value="#{fooSetBean.value}" var="item" id="repeat">
        <cc:insertChildren/>
        <h:commandButton value="remove" action="#{cc.remove(item)}"/>
    </ui:repeat>
    <h:commandButton value="add" action="#{cc.add()}"/>
</cc:implementation>

with a EditRepeatComponent.java

@FacesComponent("my.app.component.EditRepeatComponent")
public class EditRepeatComponent extends UIInput implements NamingContainer {

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        Collection value = (Collection) super.getAttributes().get("value");
        List<Foo> list = new ArrayList<>(value);
        setList(list);
        super.encodeBegin(context);
    }

    public List getList() {
        return (List) getStateHelper().get("list");
    }

    public void setList(List list) {
        getStateHelper().put("list", list);
    }

    public void add() {
        try {
            Class itemClass = (Class) super.getAttributes().get("itemClass");
            Object newItem = itemClass.newInstance();
            getList().add(newItem);
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void remove(Object item) {
        getList().remove(item);
    }

    @Override
    public void updateModel(FacesContext context) {
        // ???
    }

    @Override
    public Object getSubmittedValue() {
        // ???
    }
}

But that doesn't work After a few seconds (the system works during 1 second) I have an exeption :

java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.get(ArrayList.java:429)
at javax.faces.component.AttachedObjectListHolder.restoreState(AttachedObjectListHolder.java:166)
at javax.faces.component.UIComponentBase.restoreState(UIComponentBase.java:1611)
at com.sun.faces.application.view.FaceletPartialStateManagementStrategy$2.visit(FaceletPartialStateManagementStrategy.java:380)
at com.sun.faces.component.visit.FullVisitContext.invokeVisitCallback(FullVisitContext.java:151)
at javax.faces.component.UIComponent.visitTree(UIComponent.java:1689)
at javax.faces.component.UIComponent.visitTree(UIComponent.java:1700)
at javax.faces.component.UIComponent.visitTree(UIComponent.java:1700)
at com.sun.faces.application.view.FaceletPartialStateManagementStrategy.restoreView(FaceletPartialStateManagementStrategy.java:367)
at com.sun.faces.application.StateManagerImpl.restoreView(StateManagerImpl.java:138)
at com.sun.faces.application.view.ViewHandlingStrategy.restoreView(ViewHandlingStrategy.java:123)
at com.sun.faces.application.view.FaceletViewHandlingStrategy.restoreView(FaceletViewHandlingStrategy.java:585)
at com.sun.faces.application.view.MultiViewHandler.restoreView(MultiViewHandler.java:150)
at javax.faces.application.ViewHandlerWrapper.restoreView(ViewHandlerWrapper.java:353)
at javax.faces.application.ViewHandlerWrapper.restoreView(ViewHandlerWrapper.java:353)
at javax.faces.application.ViewHandlerWrapper.restoreView(ViewHandlerWrapper.java:353)
at org.omnifaces.viewhandler.RestorableViewHandler.restoreView(RestorableViewHandler.java:86)
at com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:197)
at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
at com.sun.faces.lifecycle.RestoreViewPhase.doPhase(RestoreViewPhase.java:121)
at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:198)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:646)
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:85)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:61)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at org.wildfly.extension.undertow.security.SecurityContextAssociationHandler.handleRequest(SecurityContextAssociationHandler.java:78)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:56)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:45)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:63)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:58)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:70)
at io.undertow.security.handlers.SecurityInitialHandler.handleRequest(SecurityInitialHandler.java:76)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:261)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:247)
at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:76)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:166)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:197)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:759)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

And I don't understand why

And I don't know yet how to implements updateModel() or getSubmittedValue() to make all the system working

kwisatz
  • 1,266
  • 3
  • 16
  • 36

1 Answers1

1

After 2 days I finally succeed

Omnifaces doc http://showcase.omnifaces.org/functions/Converters gave me the solution to deal with ui:repeat : using toArray()

    <h:form>
        <ez:editRepeat value="#{someBean.myModel.fooSet}" itemClass="my.app.model.Foo">
            <h:outputLabel value="Foo label: "/>
            <h:inputText value="#{item.label}"/>
        </ez:editRepeat>
        <ez:editRepeat value="#{someBean.myModel.barSet}" itemClass="my.app.model.Bar">
            <h:outputLabel value="Bar name: "/>
            <h:inputText value="#{item.name}"/>
        </ez:editRepeat>
        <!-- ... -->
    </h:form>

the editRepeat.xtml (I'm using primefaces p:commanButton to specify update and process attribute so that I don't lose unsubmited input and I don't submit all the form)

<cc:interface>
    <cc:attribute name="value" type="java.util.Collection" required="true"/>
    <cc:attribute name="itemClass" type="java.lang.String" required="true"/>
</cc:interface>
<cc:implementation>
    <h:panelGroup style="display: block; background-color:  rgba(200, 200, 200, 0.5); padding: 12px;">
        <ui:repeat value="#{cc.attrs.value.toArray()}" var="item">
            <h:panelGroup style="background-color: rgba(200, 200, 200, 0.5); margin-left: 12px; margin-bottom: 12px; display: block; padding: 12px;">
                <cc:insertChildren/>
                <p:commandButton value="remove" action="#{editRepeatBean.remove(cc.attrs.value, item)}"
                                 update="@parent:@parent:@parent" process="@parent:@parent:@parent"
                                 style="margin-left: 12px;"/>
            </h:panelGroup>
        </ui:repeat>
        <p:commandButton value="add" action="#{editRepeatBean.add(cc.attrs.value, cc.attrs.itemClass)}" update="@parent" process="@parent"/>
    </h:panelGroup>
</cc:implementation>

the EditRepeatBean.java

@Named
@RequestScoped
public class EditRepeatBean {

    public void add(Collection collection, String itemClassName) {
        try {
            Class itemClass = Class.forName(itemClassName);
            Object item = itemClass.newInstance();
            collection.add(item);
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void remove(Collection collection, Object item) {
        collection.remove(item);
    }

}

and if you have :

public class MyModel {
    private Set<Foo> fooSet;
    // getter and setter
}

public class Foo {
    private String label;
    private Set<Bar> barSet;
    // getters and setters
}

public class Bar {
    private String name;
    // getter and setter
}

you can do

    <h:form>
        <ez:editRepeat value="#{someBean.myModel.fooSet}" itemClass="my.app.model.Foo">
            <h:outputLabel value="Foo label: "/>
            <h:inputText value="#{item.label}"/>
            <ez:editRepeat value="#{item.barSet}" itemClass="my.app.model.Bar">
                <h:outputLabel value="Bar name: "/>
                <h:inputText value="#{item.name}"/>
            </ez:editRepeat>
        </ez:editRepeat>
        <!-- ... -->
    </h:form>

and it's working too

There is still one problem remaining : the Set must not be null, I will edit if I found a solution


EDIT : solution for the null Collection Just change the editRepeat.xhtml interface to add a componentType so that the collection will be initialize in the encodeBegin() method and add an cc:attribute to secify the implementation of a Collection with a default value to HashSet

<cc:interface componentType="my.app.component.EditRepeatComponent">
    <cc:attribute name="value" type="java.util.Collection" required="true"/>
    <cc:attribute name="itemClass" type="java.lang.String" required="true"/>
    <cc:attribute name="collectionImpl" type="java.lang.String" default="java.util.HashSet"/>
</cc:interface>

and the EditRepeatComponent.java

@FacesComponent("my.app.component.EditRepeatComponent")
public class EditRepeatComponent extends UIInput implements NamingContainer {

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        ELContext elContext = context.getELContext();
        ValueExpression valueExpression = super.getValueExpression("value");
        if (valueExpression.getValue(elContext) == null) {
            try {
                String collectionImpl = (String) super.getAttributes().get("collectionImpl");
                Class<? extends Collection> collectionClass = (Class<? extends Collection>) Class.forName(collectionImpl);
                Collection collection = collectionClass.newInstance();
                valueExpression.setValue(elContext, collection);
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
                throw new RuntimeException(ex);
            }
        }
        super.encodeBegin(context);
    }

}

well there is still a problem... when an ez:editRepeat is in an other ez:editRepeat, the remove of the inner ez:editRepeat doesn't work

Caused by: javax.el.PropertyNotFoundException: The class 'my.app.model.Bar' does not have the property 'barSet'

Edit : Final solution.

With the previous solution, there was a problem in nested <editRepeat>, during the processValidators() phase, the var of inner <repeat> components is null, causing a Exception and I don't know why, it's may be a bug...

The solution is to @Override processValidators() and re set the repeat.var.

Here is the complete solution with some improvement :

  • The component is warpped in an other so that the update of the parent just update the component
  • All the code of EditRepeatBean has been moved to EditRepeatComponent
  • Add var attribute
  • rename attributes for consistency
  • update/render and process/execute done programmatically

    <h:form>
        <ez:editRepeat value="#{someBean.myModel.fooSet}"
                itemType="my.app.model.Foo"
                var="foo">
            <h:outputLabel value="Foo label: "/>
            <h:inputText value="#{foo.label}"/>
            <ez:editRepeat value="#{foo.barSet}"
                    itemType="my.app.model.Bar"
                    var="bar">
                <h:outputLabel value="Bar name: "/>
                <h:inputText value="#{bar.name}"/>
            </ez:editRepeat>
        </ez:editRepeat>
        <!-- ... -->
    </h:form>
    

editRepeat.xhtml (the wrapper) :

<cc:interface>
    <cc:attribute name="value" type="java.util.Collection" required="true"/>
    <cc:attribute name="itemType" type="java.lang.String" required="true"/>
    <cc:attribute name="collectionType" type="java.lang.String" default="java.util.HashSet"/>
    <cc:attribute name="var" type="java.lang.String" required="true"/>
</cc:interface>

<cc:implementation>
    <h:panelGroup id="#{cc.id}Wrapper">
        <ez:editRepeatWrapped value="#{cc.attrs.value}" var="#{cc.attrs.var}"
                       itemType="#{cc.attrs.itemType}"
                       collectionType="#{cc.attrs.collectionType}"
                       id="#{cc.id}Wrapped">
            <cc:insertChildren/>
        </ez:editRepeatWrapped>
    </h:panelGroup>
</cc:implementation>

the editRepeatWrapped.xhtml :

<cc:interface componentType="my.app.component.EditRepeatComponent">
    <cc:attribute name="value" type="java.util.Collection" required="true"/>
    <cc:attribute name="itemType" type="java.lang.String" required="true"/>
    <cc:attribute name="collectionType" type="java.lang.String" default="java.util.HashSet"/>
    <cc:attribute name="var" type="java.lang.String" required="true"/>
</cc:interface>

<cc:implementation>
    <h:panelGroup id="itemsGroup" style="display: block; background-color:  rgba(0, 255, 0, 0.20); padding: 6px; margin: 6px;">
        <ui:repeat value="#{cc.attrs.value.toArray()}" var="#{cc.attrs.var}"
                id="#{cc.attrs.id}Repeat">
            <h:panelGroup id="itemGroup" style="background-color: rgba(0, 255, 0, 0.2); margin-left: 12px; margin: 6px; display: block; padding: 6px;">
                <cc:insertChildren/>
                <p:commandButton value="remove" action="#{cc.remove()}"
                                 style="margin-left: 12px;"/>
            </h:panelGroup>
        </ui:repeat>
        <p:commandButton value="add" action="#{cc.add()}"/>
    </h:panelGroup>
</cc:implementation>

the EditeRepeatComponent.java :

@FacesComponent("my.app.component.EditRepeatComponent")
public class EditRepeatComponent extends UIInput implements NamingContainer {

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void processValidators(FacesContext context) {
        initVar(); // because repeat.var is null at this stage
        super.processValidators(context);
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        initValue(context);
        initVar();
        super.encodeBegin(context);
    }

    /**
     * set var of the repeat component
     */
    private void initVar() {
        String idRepeatComponent = ((String) super.getAttributes().get("id")) + "Repeat";
        String var = (String) getAttributes().get("var");
        UIRepeat repeatConponent = (UIRepeat) super.findComponent(idRepeatComponent);
        repeatConponent.setVar(var);
    }

    /**
     * if the value is null then initialize the collection with the collection type attribute
     */
    private void initValue(FacesContext context) {
        ELContext elContext = context.getELContext();
        ValueExpression valueExpression = super.getValueExpression("value");
        Collection collection = (Collection) valueExpression.getValue(elContext);
        if (collection == null) {
            try {
                String collectionType = (String) getAttributes().get("collectionType");
                Class<? extends Collection> collectionClass = (Class<? extends Collection>) Class.forName(collectionType);
                collection = collectionClass.newInstance();
                valueExpression.setValue(elContext, collection);
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    public void remove() {
        String var = (String) getAttributes().get("var");
        Object item = evaluate(var);
        Collection collection = (Collection) getAttributes().get("value");
        collection.remove(item);
        updateView();
    }

    private Object evaluate(String var) {
        FacesContext facesContext = getFacesContext();
        ELContext elContext = facesContext.getELContext();
        Application application = facesContext.getApplication();
        ExpressionFactory expressionFactory = application.getExpressionFactory();
        ValueExpression expression = expressionFactory.createValueExpression(elContext, "#{" + var + "}", Object.class);
        Object item = expression.getValue(elContext);
        return item;
    }

    public void add() {
        try {
            Collection collection = (Collection) getAttributes().get("value");
            String itemType = (String) getAttributes().get("itemType");
            Class itemClass = Class.forName(itemType);
            Object item = itemClass.newInstance();
            collection.add(item);
            updateView();
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * render/update and execute/process the wrapper of the component
     */
    private void updateView() {
        PartialViewContext context = getFacesContext().getPartialViewContext();
        String parentId = this.getParent().getClientId();
        context.getRenderIds().add(parentId);
        context.getExecuteIds().add(parentId);
    }

}

Not that <ui:repeat ... var="#{cc.attrs.var}" ...> is useless, the var isn't set that way (and I don't know why...), it is set in the EditRepeatComponent.initVar() during both encodeBegin() and processValidators() I just put var="#{cc.attrs.var}" for the understanding

kwisatz
  • 1,266
  • 3
  • 16
  • 36