Multiple asynchronous ajax inside expanded row of Primefaces dataTable

670 Views Asked by At

The code below simulates the bad use of LazyDataModel during the traversing component tree across threads.

//Car.java
public class Car implements Serializable {

    private Integer id;
    private String manufacturer;
    private String type;

    public Car(Integer id, String manufacturer, String type) {
        this.id = id;
        this.manufacturer = manufacturer;
        this.type = type;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}
//CarLazyDataModel.java
public class CarLazyDataModel extends LazyDataModel<Car> {

    private final List<Car> datasource = new ArrayList<>();

    public CarLazyDataModel() {
        datasource.add(new Car(0, "BMW","x1"));
        datasource.add(new Car(1, "BMW","x2"));
        datasource.add(new Car(2, "BMW","x3"));
        datasource.add(new Car(3, "BMW","x4"));
        datasource.add(new Car(4, "BMW","x5"));
        datasource.add(new Car(5, "Skoda","fabia"));
        datasource.add(new Car(6, "Skoda","octavia"));
        datasource.add(new Car(7, "Skoda","superb"));
        datasource.add(new Car(8, "Skoda","roomster"));
        datasource.add(new Car(9, "Skoda","yeti"));
        datasource.add(new Car(10, "Skoda","karoq"));
        datasource.add(new Car(11, "Skoda","kodiaq"));
        datasource.add(new Car(12, "Skoda","scala"));
        datasource.add(new Car(13, "Skoda","citygo"));
        datasource.add(new Car(14, "Audi","a1"));
        datasource.add(new Car(15, "Audi","a2"));
        datasource.add(new Car(16, "Audi","a3"));
        datasource.add(new Car(17, "Audi","a4"));
    }

    @Override
    public Car getRowData(String rowKey) {
        for (Car car : datasource) {
            if (car.getId().equals(rowKey)) {
                return car;
            }
        }
        return null;
    }

    @Override
    public Object getRowKey(Car car) {
        return car.getId();
    }

    @Override
    public List<Car> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) {
        //rowCount
        int dataSize = datasource.size();
        this.setRowCount(dataSize);

        //paginate
        if (dataSize > pageSize) {
            try {
                return datasource.subList(first, first + pageSize);
            }
            catch (IndexOutOfBoundsException e) {
                return datasource.subList(first, first + (dataSize % pageSize));
            }
        }
        else {
            return datasource;
        }
    }
}
//CarsBean.java
@Named
@ViewScoped
public class CarsBean implements Serializable {

    private Integer expansionCounter1 = 0;
    private Integer expansionCounter2 = 0;
    private Integer expansionCounter3 = 0;
    private Integer expansionCounter4 = 0;

    private CarLazyDataModel carModel = new CarLazyDataModel();

    public CarLazyDataModel getCarModel() {
        return carModel;
    }

    public Integer getExpansionCounter1() {
        return expansionCounter1;
    }

    public Integer getExpansionCounter2() {
        return expansionCounter2;
    }

    public Integer getExpansionCounter3() {
        return expansionCounter3;
    }

    public Integer getExpansionCounter4() {
        return expansionCounter4;
    }

    public void incExpansionCounter1() {
        expansionCounter1++;
    }

    public void incExpansionCounter2() {
        expansionCounter2++;
    }

    public void incExpansionCounter3() {
        expansionCounter3++;
    }

    public void incExpansionCounter4() {
        expansionCounter4++;
    }
}
<!DOCTYPE HTML>
<!-- cars.xhtml -->
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui">

    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
    </h:head>

    <h:body>
        <h:form id="form">
            <p:dataTable id="table" value="#{carsBean.carModel}" var="car" lazy="true"
                         paginator="true"
                         paginatorPosition="bottom"
                         rowExpandMode="single"
                         rows="5"
                         rowsPerPageTemplate="5,10,15"
                         pageLinks="5">

                <p:column>
                    <p:rowToggler/>
                </p:column>

                <p:column headerText="ID">
                    <h:outputText value="#{car.id}"/>
                </p:column>

                <p:column headerText="Manufacturer">
                    <h:outputText value="#{car.manufacturer}"/>
                </p:column>

                <p:column headerText="Type">
                    <h:outputText value="#{car.type}"/>
                </p:column>

                <p:rowExpansion>
                    <h:outputText id="expansionCounter1" value="#{carsBean.expansionCounter1}"/>

                    <h:outputText id="expansionCounter2" value="#{carsBean.expansionCounter2}"/>

                    <h:outputText id="expansionCounter3" value="#{carsBean.expansionCounter3}"/>

                    <h:outputText id="expansionCounter4" value="#{carsBean.expansionCounter4}"/>

                    <p:remoteCommand async="true" autoRun="true" actionListener="#{carsBean.incExpansionCounter1()}" update="expansionCounter1" process="@this"/>
                    <p:remoteCommand async="true" autoRun="true" actionListener="#{carsBean.incExpansionCounter2()}" update="expansionCounter2" process="@this"/>
                    <p:remoteCommand async="true" autoRun="true" actionListener="#{carsBean.incExpansionCounter3()}" update="expansionCounter3" process="@this"/>
                    <p:remoteCommand async="true" autoRun="true" actionListener="#{carsBean.incExpansionCounter4()}" update="expansionCounter4" process="@this"/>
                </p:rowExpansion>
            </p:dataTable>
        </h:form>
    </h:body>
</html>

Run the project, open cars.xhtml page and run this js code snippet in console, which causes sometimes IndexOutOfBoundsException or empty response. I expect that expanded row will have the same counters forever. I have different counters in the 10th cycle, but it's always different.

var autoToggle = setInterval(function(){
     //expand
    $('#form\\:table tbody tr:first-child .ui-row-toggler').click();
    setTimeout(function(){ 
        if ($('#form\\:table\\:0\\:expansionCounter1').text() ==
            $('#form\\:table\\:0\\:expansionCounter2').text() && 
            $('#form\\:table\\:0\\:expansionCounter3').text() == 
            $('#form\\:table\\:0\\:expansionCounter4').text() &&
            $('#form\\:table\\:0\\:expansionCounter1').text() ==
            $('#form\\:table\\:0\\:expansionCounter4').text()) {
            //hide
            $('#form\\:table tbody tr:first-child .ui-row-toggler').click();
        } else {
            //stop
            clearInterval(autoToggle);
        }
    }, 1000);
}, 2000);

Here is IndexOutOfBoundException which you can get:

com.sun.faces.context.AjaxExceptionHandlerImpl.handlePartialResponseError java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 5
    at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
    at java.base/java.util.Objects.checkIndex(Objects.java:372)
    at java.base/java.util.ArrayList$SubList.get(ArrayList.java:1178)
    at org.primefaces.model.LazyDataModel.getRowData(LazyDataModel.java:65)
    at org.primefaces.model.LazyDataModel.setRowIndex(LazyDataModel.java:92)
    at org.primefaces.component.api.UIData.setRowModel(UIData.java:597)
    at org.primefaces.component.api.UIData.setRowIndexWithoutRowStatePreserved(UIData.java:590)
    at org.primefaces.component.api.UIData.setRowIndex(UIData.java:650)
    at org.primefaces.component.datatable.DataTable.processChildren(DataTable.java:1246)
    at org.primefaces.component.api.UIData.processPhase(UIData.java:370)
    at org.primefaces.component.api.UIData.processDecodes(UIData.java:328)
    at javax.faces.component.UIForm.processDecodes(UIForm.java:196)

The empty response of remoteCommand look like this:

<?xml version='1.0' encoding='UTF-8'?>
<partial-response><changes><update id="j_id1:javax.faces.ViewState:0"><![CDATA[-7448117481786604217:-2837643663979757383]]></update></changes></partial-response>

What is the cause of problem?

During traversing tree of components, threads modify LazyDataModel to each other - setRowIndex in org.primefaces.components.api.UIData.java function: visitRows. If rowIndex == -1 traversing is finished, response (sometimes empty) is rendered.

My question is, have you ever seen this behavior? Is it possible to do asynchronous multiple data loading inside expanded row, when threads have to traverse over shared LazyDataModel?

I use primefaces 7.0.12 and JSF 2.3.14

1

There are 1 best solutions below

2
On

Having one lazydatamodel can be very slow on performance but having 3 is a little bit suicidal. I think that your approach is not good. With row select on main table you will fetch that two lists for other tables. Did you try something like this?

<p:dataTable id="mainDt" value="#{bean.lazyDataModel}" lazy="true" var="item">
 <!-- columns -->
<p:rowExpansion>

<p:dataTable id="dt1" value="#{item.list1}" widgetVar="ldm1Var" lazy="true" var="item2">
  <p:ajax event="filter" process="@this" immediate="true" async="true"/>
  <!-- columns -->
</p:dataTable/>

<p:dataTable id="dt2" value="#{item.list2}" widgetVar="ldm2Var" lazy="true" var="item3">
  <p:ajax event="filter" process="@this" immediate="true" async="true"/>
  <!-- columns -->
</p:dataTable/>

<script type="text/javascript">
   $(function(){
      //lazy loaders
      PF('ldm1Var').filter();
      PF('ldm2Var').filter();
   });
</script>