Repository: primefaces/primefaces
Pull request #7525 (2021-07-02 18:50:24)

Implement #7523: TreeTable: provide integrationtests
also adds generics to TreeNode

christophs78: @tandraschko , @Rapster , @melloware - What´s the expected behaviour for multisort within treetable?
I´m currently a little bit surprised what showcase does. Are only leafes sorted or????
![image](https://user-images.githubusercontent.com/10461942/124396520-6642d700-dd0a-11eb-8475-96b8d326fc9b.png)
melloware: I would expect it to "sub-sort" the column within the main sort . So if I sort "Size" first it groups the 150KB together nd then if you make Name the second sort it flips flips the values sorted in the Name column.
christophs78: Please take a second look.
IMO TreeTable does not take sortPriority into account. The later column always wins. So size first (column 2) and name second (column 1) works.
But not name first (column 1) and size second (column 2).
Look at https://github.com/primefaces/primefaces/blob/1f60dda906c252c33865fe38b7614662b8ef1308/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableRenderer.java#L1187 and https://github.com/primefaces/primefaces/blob/2e79cbc628fa98885cad4688813fc7e82f6f3a97/primefaces/src/main/java/org/primefaces/model/TreeNodeComparator.java#L54.
IMO there´s currently a mismatch between what UI and showcase suggests and what happens in reality.
melloware: Ahhh OK.   I think whatever the Showcase is doing for 10.0.0 is correct so something must have changed?
tandraschko: maybe related to this commits: https://github.com/primefaces/primefaces/issues/2672
we partly also reworked TreeTable but it was not as good tested as DataTable
christophs78: > maybe related to this commits: #2672
> we partly also reworked TreeTable but it was not as good tested as DataTable

Yes. It looks to be related to https://github.com/primefaces/primefaces/issues/2672. I'll open a separate issue for this.
tandraschko: Perfect, let me know when we can merge this

Patch:
diff --git a/pom.xml b/pom.xml
index e9b78d0bd7..e49c2b44f3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -227,7 +227,7 @@
             <plugin>
                 <groupId>com.mycila</groupId>
                 <artifactId>license-maven-plugin</artifactId>
-                <version>4.0.rc2</version>
+                <version>4.1</version>
                 <configuration>
                     <header>${project.basedir}/conf/header.txt</header>
                     <includes>
@@ -368,7 +368,7 @@
             <dependency>
                 <groupId>com.rometools</groupId>
                 <artifactId>rome</artifactId>
-                <version>1.15.0</version>
+                <version>1.16.0</version>
             </dependency>
 
             <!-- File Upload -->
diff --git a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/DataTable.java b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/DataTable.java
index 77c2bd6d4f..5d654fa080 100644
--- a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/DataTable.java
+++ b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/DataTable.java
@@ -23,30 +23,18 @@
  */
 package org.primefaces.selenium.component;
 
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import org.json.JSONObject;
 import org.openqa.selenium.By;
-import org.openqa.selenium.WebElement;
-import org.primefaces.selenium.PrimeSelenium;
-import org.primefaces.selenium.component.base.AbstractPageableData;
-import org.primefaces.selenium.component.base.ComponentUtils;
+import org.primefaces.selenium.component.base.AbstractTable;
 import org.primefaces.selenium.component.model.datatable.Cell;
-import org.primefaces.selenium.component.model.datatable.Header;
-import org.primefaces.selenium.component.model.datatable.HeaderCell;
 import org.primefaces.selenium.component.model.datatable.Row;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 /**
  * Component wrapper for the PrimeFaces {@code p:dataTable}.
  */
-public abstract class DataTable extends AbstractPageableData {
-
-    @Override
-    public List<WebElement> getRowsWebElement() {
-        return findElement(By.tagName("table")).findElement(By.tagName("tbody")).findElements(By.tagName("tr"));
-    }
+public abstract class DataTable extends AbstractTable<Row> {
 
     public List<Row> getRows() {
         return getRowsWebElement().stream().map(rowElt -> {
@@ -55,148 +43,8 @@ public abstract class DataTable extends AbstractPageableData {
         }).collect(Collectors.toList());
     }
 
+    @Override
     public Row getRow(int index) {
         return getRows().get(index);
     }
-
-    /**
-     * Gets the Cell at the row/column coordinates.
-     *
-     * @param rowIndex the index of the row
-     * @param colIndex the index of the column in the row
-     * @return the {@link Cell} representing these coordinates
-     * @throws IndexOutOfBoundsException if either row or column not found
-     */
-    public Cell getCell(int rowIndex, int colIndex) throws IndexOutOfBoundsException {
-        Row row = getRow(rowIndex);
-        if (row == null) {
-            throw new IndexOutOfBoundsException("Row " + rowIndex + " was not found in table");
-        }
-        Cell cell = row.getCell(colIndex);
-        if (cell == null) {
-            throw new IndexOutOfBoundsException("Column " + colIndex + " was not found in Row " + rowIndex + ".");
-        }
-        return cell;
-    }
-
-    public WebElement getHeaderWebElement() {
-        return findElement(By.tagName("table")).findElement(By.tagName("thead"));
-    }
-
-    public Header getHeader() {
-        List<HeaderCell> cells = getHeaderWebElement().findElements(By.tagName("th")).stream()
-                    .map(cellElt -> new HeaderCell(cellElt))
-                    .collect(Collectors.toList());
-        return new Header(getHeaderWebElement(), cells);
-    }
-
-    /**
-     * Sorts the column found by its header text. It toggles to the next sort direction.
-     *
-     * @param headerText the header text to look for
-     */
-    public void sort(String headerText) {
-        Optional<HeaderCell> cell = getHeader().getCell(headerText);
-        if (cell.isPresent()) {
-            PrimeSelenium.guardAjax(cell.get().getWebElement().findElement(By.className("ui-sortable-column-icon"))).click();
-        }
-        else {
-            System.err.println("Header Cell '" + headerText + "' not found.");
-        }
-    }
-
-    /**
-     * Sorts the column found by its index. It toggles to the next sort direction.
-     *
-     * @param index the index of the column
-     */
-    public void sort(int index) {
-        HeaderCell cell = getHeader().getCell(index);
-        if (cell != null) {
-            PrimeSelenium.guardAjax(cell.getWebElement().findElement(By.className("ui-sortable-column-icon"))).click();
-        }
-        else {
-            System.err.println("Header Cell '" + index + "' not found.");
-        }
-    }
-
-    /**
-     * Filter the column by its index.
-     *
-     * @param cellIndex the index of the column
-     * @param filterValue the value to pass to the filter
-     */
-    public void filter(int cellIndex, String filterValue) {
-        filter(getHeader().getCell(cellIndex), filterValue);
-    }
-
-    /**
-     * Filter the column by its header text.
-     *
-     * @param headerText the header text to look for
-     * @param filterValue the value to pass to the filter
-     */
-    public void filter(String headerText, String filterValue) {
-        Optional<HeaderCell> cell = getHeader().getCell(headerText);
-        if (cell.isPresent()) {
-            filter(cell.get(), filterValue);
-        }
-        else {
-            System.err.println("Header Cell '" + headerText + "' not found.");
-        }
-    }
-
-    /**
-     * Removes the current filter at this column index.
-     *
-     * @param cellIndex the index of the column
-     */
-    public void removeFilter(int cellIndex) {
-        filter(cellIndex, null);
-    }
-
-    /**
-     * Removes the current filter for a column with the header text
-     *
-     * @param headerText the header text to look for
-     */
-    public void removeFilter(String headerText) {
-        filter(headerText, null);
-    }
-
-    /**
-     * Filter using the widget configuration for "filterDelay" and "filterEvent".
-     *
-     * @param cell the cell to filter
-     * @param filterValue the value to pass to the filter.
-     */
-    private void filter(HeaderCell cell, String filterValue) {
-        JSONObject cfg = getWidgetConfiguration();
-        if (filterValue != null && filterValue.isEmpty()) {
-            filterValue = null;
-        }
-        cell.setFilterValue(cfg, filterValue);
-    }
-
-    /**
-     * If using multiple checkbox mode this toggles the Select All checkbox in the header.
-     */
-    public void toggleSelectAllCheckBox() {
-        WebElement checkboxAll = getSelectAllCheckBox();
-        if (ComponentUtils.hasBehavior(this, "rowSelect") || ComponentUtils.hasBehavior(this, "rowUnselect")) {
-            PrimeSelenium.guardAjax(checkboxAll).click();
-        }
-        else {
-            checkboxAll.click();
-        }
-    }
-
-    /**
-     * Gets the Select All checkbox in the header of the table.
-     *
-     * @return the WebElement representing the checkbox
-     */
-    public WebElement getSelectAllCheckBox() {
-        return getHeader().getCell(0).getWebElement();
-    }
 }
diff --git a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/base/AbstractTable.java b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/base/AbstractTable.java
new file mode 100644
index 0000000000..5e3d8e1972
--- /dev/null
+++ b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/base/AbstractTable.java
@@ -0,0 +1,188 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.selenium.component.base;
+
+import org.json.JSONObject;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.primefaces.selenium.PrimeSelenium;
+import org.primefaces.selenium.component.model.datatable.Cell;
+import org.primefaces.selenium.component.model.datatable.Header;
+import org.primefaces.selenium.component.model.datatable.HeaderCell;
+import org.primefaces.selenium.component.model.datatable.Row;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public abstract class AbstractTable<T extends Row> extends AbstractPageableData {
+
+    @Override
+    public List<WebElement> getRowsWebElement() {
+        return findElement(By.tagName("table")).findElement(By.tagName("tbody")).findElements(By.tagName("tr"));
+    }
+
+    public abstract T getRow(int index);
+
+    /**
+     * Gets the Cell at the row/column coordinates.
+     *
+     * @param rowIndex the index of the row
+     * @param colIndex the index of the column in the row
+     * @return the {@link Cell} representing these coordinates
+     * @throws IndexOutOfBoundsException if either row or column not found
+     */
+    public Cell getCell(int rowIndex, int colIndex) throws IndexOutOfBoundsException {
+        Row row = getRow(rowIndex);
+        if (row == null) {
+            throw new IndexOutOfBoundsException("Row " + rowIndex + " was not found in table");
+        }
+        Cell cell = row.getCell(colIndex);
+        if (cell == null) {
+            throw new IndexOutOfBoundsException("Column " + colIndex + " was not found in Row " + rowIndex + ".");
+        }
+        return cell;
+    }
+
+    public WebElement getHeaderWebElement() {
+        return findElement(By.tagName("table")).findElement(By.tagName("thead"));
+    }
+
+    public Header getHeader() {
+        List<HeaderCell> cells = getHeaderWebElement().findElements(By.tagName("th")).stream()
+                .map(HeaderCell::new)
+                .collect(Collectors.toList());
+        return new Header(getHeaderWebElement(), cells);
+    }
+
+    /**
+     * Sorts the column found by its header text. It toggles to the next sort direction.
+     *
+     * @param headerText the header text to look for
+     */
+    public void sort(String headerText) {
+        Optional<HeaderCell> cell = getHeader().getCell(headerText);
+        if (cell.isPresent()) {
+            PrimeSelenium.guardAjax(cell.get().getWebElement().findElement(By.className("ui-sortable-column-icon"))).click();
+        }
+        else {
+            System.err.println("Header Cell '" + headerText + "' not found.");
+        }
+    }
+
+    /**
+     * Sorts the column found by its index. It toggles to the next sort direction.
+     *
+     * @param index the index of the column
+     */
+    public void sort(int index) {
+        HeaderCell cell = getHeader().getCell(index);
+        if (cell != null) {
+            PrimeSelenium.guardAjax(cell.getWebElement().findElement(By.className("ui-sortable-column-icon"))).click();
+        }
+        else {
+            System.err.println("Header Cell '" + index + "' not found.");
+        }
+    }
+
+    /**
+     * Filter the column by its index.
+     *
+     * @param cellIndex the index of the column
+     * @param filterValue the value to pass to the filter
+     */
+    public void filter(int cellIndex, String filterValue) {
+        filter(getHeader().getCell(cellIndex), filterValue);
+    }
+
+    /**
+     * Filter the column by its header text.
+     *
+     * @param headerText the header text to look for
+     * @param filterValue the value to pass to the filter
+     */
+    public void filter(String headerText, String filterValue) {
+        Optional<HeaderCell> cell = getHeader().getCell(headerText);
+        if (cell.isPresent()) {
+            filter(cell.get(), filterValue);
+        }
+        else {
+            System.err.println("Header Cell '" + headerText + "' not found.");
+        }
+    }
+
+    /**
+     * Removes the current filter at this column index.
+     *
+     * @param cellIndex the index of the column
+     */
+    public void removeFilter(int cellIndex) {
+        filter(cellIndex, null);
+    }
+
+    /**
+     * Removes the current filter for a column with the header text
+     *
+     * @param headerText the header text to look for
+     */
+    public void removeFilter(String headerText) {
+        filter(headerText, null);
+    }
+
+    /**
+     * Filter using the widget configuration for "filterDelay" and "filterEvent".
+     *
+     * @param cell the cell to filter
+     * @param filterValue the value to pass to the filter.
+     */
+    private void filter(HeaderCell cell, String filterValue) {
+        JSONObject cfg = getWidgetConfiguration();
+        if (filterValue != null && filterValue.isEmpty()) {
+            filterValue = null;
+        }
+        cell.setFilterValue(cfg, filterValue);
+    }
+
+    /**
+     * If using multiple checkbox mode this toggles the Select All checkbox in the header.
+     */
+    public void toggleSelectAllCheckBox() {
+        WebElement checkboxAll = getSelectAllCheckBox();
+        if (ComponentUtils.hasBehavior(this, "rowSelect") || ComponentUtils.hasBehavior(this, "rowUnselect")) {
+            PrimeSelenium.guardAjax(checkboxAll).click();
+        }
+        else {
+            checkboxAll.click();
+        }
+    }
+
+    /**
+     * Gets the Select All checkbox in the header of the table.
+     *
+     * @return the WebElement representing the checkbox
+     */
+    public WebElement getSelectAllCheckBox() {
+        return getHeader().getCell(0).getWebElement();
+    }
+}
diff --git a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/model/TreeTable.java b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/model/TreeTable.java
new file mode 100644
index 0000000000..a75fd5b26e
--- /dev/null
+++ b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/model/TreeTable.java
@@ -0,0 +1,50 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.selenium.component.model;
+
+import org.openqa.selenium.By;
+import org.primefaces.selenium.component.base.AbstractTable;
+import org.primefaces.selenium.component.model.datatable.Cell;
+import org.primefaces.selenium.component.model.treetable.Row;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Component wrapper for the PrimeFaces {@code p:treeTable}.
+ */
+public abstract class TreeTable extends AbstractTable<Row> {
+
+    public List<Row> getRows() {
+        return getRowsWebElement().stream().map(rowElt -> {
+            List<Cell> cells = rowElt.findElements(By.tagName("td")).stream().map(cellElt -> new Cell(cellElt)).collect(Collectors.toList());
+            return new Row(rowElt, cells);
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public Row getRow(int index) {
+        return getRows().get(index);
+    }
+}
diff --git a/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/model/treetable/Row.java b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/model/treetable/Row.java
new file mode 100644
index 0000000000..d4b40730e8
--- /dev/null
+++ b/primefaces-selenium/primefaces-selenium-components/src/main/java/org/primefaces/selenium/component/model/treetable/Row.java
@@ -0,0 +1,66 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.selenium.component.model.treetable;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.primefaces.selenium.PrimeSelenium;
+import org.primefaces.selenium.component.model.datatable.Cell;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+public class Row extends org.primefaces.selenium.component.model.datatable.Row {
+
+    public Row(WebElement webElement, List<Cell> cells) {
+        super(webElement, cells);
+    }
+
+    public boolean isToggleable() {
+        return (getToggler() != null);
+    }
+
+    public void toggle() {
+        if (isToggleable()) {
+            PrimeSelenium.guardAjax(getToggler()).click();
+        }
+    }
+
+    private WebElement getToggler() {
+        return getCell(0).getWebElement().findElement(By.className("ui-treetable-toggler"));
+    }
+
+    public int getLevel() {
+        String cssClasses = getWebElement().getAttribute("class");
+        Optional<String> levelClassOpt = Arrays.stream(cssClasses.split(" ")).filter(c -> c.startsWith("ui-node-level")).findFirst();
+        if (levelClassOpt.isPresent()) {
+            String levelClass = levelClassOpt.get();
+            return Integer.parseInt(levelClass.replace("ui-node-level-", ""));
+        }
+        else {
+            return -1;
+        }
+    }
+}
diff --git a/primefaces-showcase/pom.xml b/primefaces-showcase/pom.xml
index 4261333957..75fe2464a0 100644
--- a/primefaces-showcase/pom.xml
+++ b/primefaces-showcase/pom.xml
@@ -103,7 +103,7 @@
         <dependency>
             <groupId>org.twitter4j</groupId>
             <artifactId>twitter4j-core</artifactId>
-            <version>3.0.5</version>
+            <version>3.0.6</version>
         </dependency>
 
         <!-- Logging -->
@@ -267,7 +267,7 @@
             <extension>
                 <groupId>org.apache.maven.wagon</groupId>
                 <artifactId>wagon-ssh</artifactId>
-                <version>2.12</version>
+                <version>3.4.3</version>
             </extension>
         </extensions>
     </build>
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/service/DocumentService.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/service/DocumentService.java
index 6ee98c3a28..da1423a8a7 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/service/DocumentService.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/service/DocumentService.java
@@ -35,8 +35,8 @@ import javax.inject.Named;
 @ApplicationScoped
 public class DocumentService {
 
-    public TreeNode createDocuments() {
-        TreeNode root = new DefaultTreeNode(new Document("Files", "-", "Folder"), null);
+    public TreeNode<Document> createDocuments() {
+        TreeNode<Document> root = new DefaultTreeNode(new Document("Files", "-", "Folder"), null);
 
         TreeNode applications = new DefaultTreeNode(new Document("Applications", "100kb", "Folder"), root);
         TreeNode cloud = new DefaultTreeNode(new Document("Cloud", "20kb", "Folder"), root);
@@ -100,8 +100,8 @@ public class DocumentService {
         return root;
     }
 
-    public TreeNode createCheckboxDocuments() {
-        TreeNode root = new CheckboxTreeNode(new Document("Files", "-", "Folder"), null);
+    public TreeNode<Document> createCheckboxDocuments() {
+        TreeNode<Document> root = new CheckboxTreeNode(new Document("Files", "-", "Folder"), null);
 
         TreeNode applications = new CheckboxTreeNode(new Document("Applications", "100kb", "Folder"), root);
         TreeNode cloud = new CheckboxTreeNode(new Document("Cloud", "20kb", "Folder"), root);
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/tree/LazyLoadingTreeNode.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/tree/LazyLoadingTreeNode.java
index 3be0f1705d..626cc68faf 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/tree/LazyLoadingTreeNode.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/tree/LazyLoadingTreeNode.java
@@ -23,13 +23,14 @@
  */
 package org.primefaces.showcase.view.data.tree;
 
+import org.primefaces.model.DefaultTreeNode;
+import org.primefaces.model.TreeNode;
+
 import java.util.List;
 import java.util.function.Function;
 import java.util.stream.Collectors;
-import org.primefaces.model.DefaultTreeNode;
-import org.primefaces.model.TreeNode;
 
-public class LazyLoadingTreeNode extends DefaultTreeNode {
+public class LazyLoadingTreeNode extends DefaultTreeNode<FileInfo> {
 
     private Function<String, List<FileInfo>> loadFunction;
     private boolean lazyLoaded;
@@ -40,7 +41,7 @@ public class LazyLoadingTreeNode extends DefaultTreeNode {
     }
 
     @Override
-    public List<TreeNode> getChildren() {
+    public List<TreeNode<FileInfo>> getChildren() {
         lazyLoad();
 
         return super.getChildren();
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/BasicView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/BasicView.java
index caa3377dda..50fddd6e65 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/BasicView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/BasicView.java
@@ -23,25 +23,25 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
+import org.primefaces.model.SortMeta;
+import org.primefaces.model.SortOrder;
 import org.primefaces.model.TreeNode;
 import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
-import org.primefaces.model.SortMeta;
-import org.primefaces.model.SortOrder;
 
 @Named("ttBasicView")
 @ViewScoped
 public class BasicView implements Serializable {
 
-    private TreeNode root;
+    private TreeNode<Document> root;
     private List<SortMeta> sortBy;
 
     private Document selectedDocument;
@@ -60,7 +60,7 @@ public class BasicView implements Serializable {
                 .build());
     }
 
-    public TreeNode getRoot() {
+    public TreeNode<Document> getRoot() {
         return root;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ColumnsView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ColumnsView.java
index 1b90041d93..a644dd7758 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ColumnsView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ColumnsView.java
@@ -23,11 +23,12 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
@@ -46,7 +47,7 @@ public class ColumnsView implements Serializable {
 
     private String columnTemplate = "name size type";
     private List<ColumnModel> columns;
-    private TreeNode root;
+    private TreeNode<Document> root;
 
     @PostConstruct
     public void init() {
@@ -55,7 +56,7 @@ public class ColumnsView implements Serializable {
         createDynamicColumns();
     }
 
-    public TreeNode getRoot() {
+    public TreeNode<Document> getRoot() {
         return root;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ContextMenuView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ContextMenuView.java
index 80d70ac411..3f589ec580 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ContextMenuView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ContextMenuView.java
@@ -23,11 +23,12 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
@@ -36,9 +37,9 @@ import java.io.Serializable;
 @ViewScoped
 public class ContextMenuView implements Serializable {
 
-    private TreeNode root;
+    private TreeNode<Document> root;
 
-    private TreeNode selectedNode;
+    private TreeNode<Document> selectedNode;
 
     @Inject
     private DocumentService service;
@@ -48,7 +49,7 @@ public class ContextMenuView implements Serializable {
         root = service.createDocuments();
     }
 
-    public TreeNode getRoot() {
+    public TreeNode<Document> getRoot() {
         return root;
     }
 
@@ -56,11 +57,11 @@ public class ContextMenuView implements Serializable {
         this.service = service;
     }
 
-    public TreeNode getSelectedNode() {
+    public TreeNode<Document> getSelectedNode() {
         return selectedNode;
     }
 
-    public void setSelectedNode(TreeNode selectedNode) {
+    public void setSelectedNode(TreeNode<Document> selectedNode) {
         this.selectedNode = selectedNode;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EditView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EditView.java
index 7d8490f59a..480c47d073 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EditView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EditView.java
@@ -26,6 +26,7 @@ package org.primefaces.showcase.view.data.treetable;
 import org.primefaces.event.CellEditEvent;
 import org.primefaces.event.RowEditEvent;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
@@ -40,11 +41,11 @@ import java.io.Serializable;
 @ViewScoped
 public class EditView implements Serializable {
 
-    private TreeNode root;
+    private TreeNode<Document> root;
 
-    private TreeNode root2;
+    private TreeNode<Document> root2;
 
-    private TreeNode root3;
+    private TreeNode<Document> root3;
 
     @Inject
     private DocumentService service;
@@ -56,15 +57,15 @@ public class EditView implements Serializable {
         root3 = service.createDocuments();
     }
 
-    public TreeNode getRoot() {
+    public TreeNode<Document> getRoot() {
         return root;
     }
 
-    public TreeNode getRoot2() {
+    public TreeNode<Document> getRoot2() {
         return root2;
     }
 
-    public TreeNode getRoot3() {
+    public TreeNode<Document> getRoot3() {
         return root3;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EventsView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EventsView.java
index 7440b68fde..9a421d9e00 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EventsView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/EventsView.java
@@ -23,30 +23,31 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.event.NodeCollapseEvent;
 import org.primefaces.event.NodeExpandEvent;
 import org.primefaces.event.NodeSelectEvent;
 import org.primefaces.event.NodeUnselectEvent;
+import org.primefaces.event.data.FilterEvent;
+import org.primefaces.event.data.SortEvent;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
 import javax.faces.application.FacesMessage;
 import javax.faces.context.FacesContext;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
-import org.primefaces.event.data.FilterEvent;
-import org.primefaces.event.data.SortEvent;
 
 @Named("ttEventsView")
 @ViewScoped
 public class EventsView implements Serializable {
 
-    private TreeNode root;
+    private TreeNode<Document> root;
 
-    private TreeNode selectedNode;
+    private TreeNode<Document> selectedNode;
 
     @Inject
     private DocumentService service;
@@ -56,15 +57,15 @@ public class EventsView implements Serializable {
         root = service.createDocuments();
     }
 
-    public TreeNode getRoot() {
+    public TreeNode<Document> getRoot() {
         return root;
     }
 
-    public TreeNode getSelectedNode() {
+    public TreeNode<Document> getSelectedNode() {
         return selectedNode;
     }
 
-    public void setSelectedNode(TreeNode selectedNode) {
+    public void setSelectedNode(TreeNode<Document> selectedNode) {
         this.selectedNode = selectedNode;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/MultiViewStateView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/MultiViewStateView.java
index f2db34fe34..2eb832a7a8 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/MultiViewStateView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/MultiViewStateView.java
@@ -23,24 +23,24 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
+import org.primefaces.PrimeFaces;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
+import javax.faces.application.FacesMessage;
+import javax.faces.context.FacesContext;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
-import javax.faces.application.FacesMessage;
-import javax.faces.context.FacesContext;
-import org.primefaces.PrimeFaces;
-import org.primefaces.showcase.domain.Document;
 
 @Named("ttMultiViewStateView")
 @ViewScoped
 public class MultiViewStateView implements Serializable {
 
-    private TreeNode root;
+    private TreeNode<Document> root;
 
     @Inject
     private DocumentService service;
@@ -50,7 +50,7 @@ public class MultiViewStateView implements Serializable {
         root = service.createDocuments();
     }
 
-    public TreeNode getRoot() {
+    public TreeNode<Document> getRoot() {
         return root;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/PaginatorView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/PaginatorView.java
index 41ba8bb4f5..60be6ed0cc 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/PaginatorView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/PaginatorView.java
@@ -23,12 +23,12 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.model.DefaultTreeNode;
 import org.primefaces.model.TreeNode;
 import org.primefaces.showcase.domain.Document;
 
 import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
 import javax.inject.Named;
 import java.io.Serializable;
 
@@ -36,7 +36,7 @@ import java.io.Serializable;
 @ViewScoped
 public class PaginatorView implements Serializable {
 
-    private TreeNode root;
+    private TreeNode<Document> root;
 
     @PostConstruct
     public void init() {
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ScrollableView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ScrollableView.java
index a4d346d4af..305b2910be 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ScrollableView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/ScrollableView.java
@@ -23,11 +23,12 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
@@ -36,9 +37,9 @@ import java.io.Serializable;
 @ViewScoped
 public class ScrollableView implements Serializable {
 
-    private TreeNode root1;
-    private TreeNode root2;
-    private TreeNode root3;
+    private TreeNode<Document> root1;
+    private TreeNode<Document> root2;
+    private TreeNode<Document> root3;
 
     @Inject
     private DocumentService service;
@@ -53,15 +54,15 @@ public class ScrollableView implements Serializable {
         root1.getChildren().get(1).setExpanded(true);
     }
 
-    public TreeNode getRoot1() {
+    public TreeNode<Document> getRoot1() {
         return root1;
     }
 
-    public TreeNode getRoot2() {
+    public TreeNode<Document> getRoot2() {
         return root2;
     }
 
-    public TreeNode getRoot3() {
+    public TreeNode<Document> getRoot3() {
         return root3;
     }
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/SelectionView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/SelectionView.java
index 04fb60dc22..e6d16730dc 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/SelectionView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/data/treetable/SelectionView.java
@@ -23,13 +23,14 @@
  */
 package org.primefaces.showcase.view.data.treetable;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.model.TreeNode;
+import org.primefaces.showcase.domain.Document;
 import org.primefaces.showcase.service.DocumentService;
 
 import javax.annotation.PostConstruct;
 import javax.faces.application.FacesMessage;
 import javax.faces.context.FacesContext;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
@@ -38,12 +39,12 @@ import java.io.Serializable;
 @ViewScoped
 public class SelectionView implements Serializable {
 
-    private TreeNode root1;
-    private TreeNode root2;
-    private TreeNode root3;
-    private TreeNode selectedNode;
-    private TreeNode[] selectedNodes1;
-    private TreeNode[] selectedNodes2;
+    private TreeNode<Document> root1;
+    private TreeNode<Document> root2;
+    private TreeNode<Document> root3;
+    private TreeNode<Document> selectedNode;
+    private TreeNode<Document>[] selectedNodes1;
+    private TreeNode<Document>[] selectedNodes2;
 
     @Inject
     private DocumentService service;
@@ -55,39 +56,39 @@ public class SelectionView implements Serializable {
         root3 = service.createCheckboxDocuments();
     }
 
-    public TreeNode getRoot1() {
+    public TreeNode<Document> getRoot1() {
         return root1;
     }
 
-    public TreeNode getRoot2() {
+    public TreeNode<Document> getRoot2() {
         return root2;
     }
 
-    public TreeNode getRoot3() {
+    public TreeNode<Document> getRoot3() {
         return root3;
     }
 
-    public TreeNode getSelectedNode() {
+    public TreeNode<Document> getSelectedNode() {
         return selectedNode;
     }
 
-    public void setSelectedNode(TreeNode selectedNode) {
+    public void setSelectedNode(TreeNode<Document> selectedNode) {
         this.selectedNode = selectedNode;
     }
 
-    public TreeNode[] getSelectedNodes1() {
+    public TreeNode<Document>[] getSelectedNodes1() {
         return selectedNodes1;
     }
 
-    public void setSelectedNodes1(TreeNode[] selectedNodes1) {
+    public void setSelectedNodes1(TreeNode<Document>[] selectedNodes1) {
         this.selectedNodes1 = selectedNodes1;
     }
 
-    public TreeNode[] getSelectedNodes2() {
+    public TreeNode<Document>[] getSelectedNodes2() {
         return selectedNodes2;
     }
 
-    public void setSelectedNodes2(TreeNode[] selectedNodes2) {
+    public void setSelectedNodes2(TreeNode<Document>[] selectedNodes2) {
         this.selectedNodes2 = selectedNodes2;
     }
 
@@ -102,7 +103,7 @@ public class SelectionView implements Serializable {
         }
     }
 
-    public void displaySelectedMultiple(TreeNode[] nodes) {
+    public void displaySelectedMultiple(TreeNode<Document>[] nodes) {
         if (nodes != null && nodes.length > 0) {
             StringBuilder builder = new StringBuilder();
 
diff --git a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/dnd/ColumnManagerView.java b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/dnd/ColumnManagerView.java
index d8de34708d..6a33896338 100644
--- a/primefaces-showcase/src/main/java/org/primefaces/showcase/view/dnd/ColumnManagerView.java
+++ b/primefaces-showcase/src/main/java/org/primefaces/showcase/view/dnd/ColumnManagerView.java
@@ -23,7 +23,6 @@
  */
 package org.primefaces.showcase.view.dnd;
 
-import javax.faces.view.ViewScoped;
 import org.primefaces.model.DefaultTreeNode;
 import org.primefaces.model.TreeNode;
 import org.primefaces.showcase.domain.Product;
@@ -31,6 +30,7 @@ import org.primefaces.showcase.service.ProductService;
 
 import javax.annotation.PostConstruct;
 import javax.faces.context.FacesContext;
+import javax.faces.view.ViewScoped;
 import javax.inject.Inject;
 import javax.inject.Named;
 import java.io.Serializable;
@@ -50,7 +50,7 @@ public class ColumnManagerView implements Serializable {
 
     private List<Product> products;
 
-    private TreeNode availableColumns;
+    private TreeNode<Product> availableColumns;
 
     @Inject
     private ProductService service;
@@ -102,7 +102,7 @@ public class ColumnManagerView implements Serializable {
         this.columns.add(dropColumnIndex, new ColumnModel(property.toUpperCase(), property));
 
         //remove from nodes
-        TreeNode root = availableColumns.getChildren().get(0);
+        TreeNode<?> root = availableColumns.getChildren().get(0);
         for (TreeNode node : root.getChildren()) {
             ColumnModel model = (ColumnModel) node.getData();
             if (model.getProperty().equals(property)) {
diff --git a/primefaces/pom.xml b/primefaces/pom.xml
index 8a6a2c4a5f..8228b8c497 100644
--- a/primefaces/pom.xml
+++ b/primefaces/pom.xml
@@ -180,12 +180,6 @@
             <artifactId>mime-types</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>xml-apis</groupId>
-            <artifactId>xml-apis</artifactId>
-            <version>1.4.01</version>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-nop</artifactId>
@@ -203,7 +197,7 @@
         <dependency>
             <groupId>net.sf.ehcache</groupId>
             <artifactId>ehcache</artifactId>
-            <version>2.7.4</version>
+            <version>2.10.9.2</version>
             <scope>provided</scope>
             <exclusions>
                 <exclusion>
@@ -356,7 +350,7 @@
             <extension>
                 <groupId>org.apache.maven.wagon</groupId>
                 <artifactId>wagon-ssh</artifactId>
-                <version>3.0.0</version>
+                <version>3.4.3</version>
             </extension>
         </extensions>
 
diff --git a/primefaces/src/main/java/org/primefaces/component/api/UITree.java b/primefaces/src/main/java/org/primefaces/component/api/UITree.java
index 6721bf77ac..d5e39b3259 100644
--- a/primefaces/src/main/java/org/primefaces/component/api/UITree.java
+++ b/primefaces/src/main/java/org/primefaces/component/api/UITree.java
@@ -23,31 +23,30 @@
  */
 package org.primefaces.component.api;
 
-import java.io.IOException;
-import java.util.*;
+import org.primefaces.component.column.Column;
+import org.primefaces.component.columngroup.ColumnGroup;
+import org.primefaces.component.columns.Columns;
+import org.primefaces.component.tree.UITreeNode;
+import org.primefaces.model.CheckboxTreeNode;
+import org.primefaces.model.TreeNode;
+import org.primefaces.util.ComponentUtils;
+import org.primefaces.util.LangUtils;
+import org.primefaces.util.MessageFactory;
+import org.primefaces.util.SharedStringBuilder;
 
 import javax.el.ValueExpression;
 import javax.faces.FacesException;
 import javax.faces.application.Application;
 import javax.faces.application.FacesMessage;
-import javax.faces.component.*;
 import javax.faces.component.UIColumn;
+import javax.faces.component.*;
 import javax.faces.component.visit.VisitCallback;
 import javax.faces.component.visit.VisitContext;
 import javax.faces.component.visit.VisitResult;
 import javax.faces.context.FacesContext;
 import javax.faces.event.*;
-
-import org.primefaces.component.column.Column;
-import org.primefaces.component.columngroup.ColumnGroup;
-import org.primefaces.component.columns.Columns;
-import org.primefaces.component.tree.UITreeNode;
-import org.primefaces.model.CheckboxTreeNode;
-import org.primefaces.model.TreeNode;
-import org.primefaces.util.ComponentUtils;
-import org.primefaces.util.LangUtils;
-import org.primefaces.util.MessageFactory;
-import org.primefaces.util.SharedStringBuilder;
+import java.io.IOException;
+import java.util.*;
 
 public abstract class UITree extends UIComponentBase implements NamingContainer {
 
@@ -229,7 +228,7 @@ public abstract class UITree extends UIComponentBase implements NamingContainer
         getStateHelper().put(PropertyKeys.propagateSelectionUp, _propagateSelectionUp);
     }
 
-    protected TreeNode findTreeNode(TreeNode searchRoot, String rowKey) {
+    protected TreeNode<?> findTreeNode(TreeNode<?> searchRoot, String rowKey) {
         if (rowKey == null || searchRoot == null) {
             return null;
         }
@@ -261,7 +260,7 @@ public abstract class UITree extends UIComponentBase implements NamingContainer
         }
     }
 
-    public void buildRowKeys(TreeNode node) {
+    public void buildRowKeys(TreeNode<?> node) {
         if (node.isExpanded() || node.getParent() == null || node.getParent().isExpanded()) {
             int childCount = node.getChildCount();
             if (childCount > 0) {
@@ -279,7 +278,7 @@ public abstract class UITree extends UIComponentBase implements NamingContainer
         }
     }
 
-    public void populateRowKeys(TreeNode node, List<String> keys) {
+    public void populateRowKeys(TreeNode<?> node, List<String> keys) {
         if (node == null) {
             return;
         }
@@ -296,7 +295,7 @@ public abstract class UITree extends UIComponentBase implements NamingContainer
         }
     }
 
-    public void updateRowKeys(TreeNode node) {
+    public void updateRowKeys(TreeNode<?> node) {
         int childCount = node.getChildCount();
         if (childCount > 0) {
             for (int i = 0; i < childCount; i++) {
@@ -335,11 +334,11 @@ public abstract class UITree extends UIComponentBase implements NamingContainer
         }
     }
 
-    private void updateSelectedNodes(TreeNode node) {
+    private void updateSelectedNodes(TreeNode<?> node) {
         int childCount = node.getChildCount();
         if (childCount > 0) {
             for (int i = 0; i < childCount; i++) {
-                TreeNode childNode = node.getChildren().get(i);
+                TreeNode<?> childNode = node.getChildren().get(i);
                 if (childNode.isSelected()) {
                     addToPreselection(childNode);
                 }
diff --git a/primefaces/src/main/java/org/primefaces/component/tree/Tree.java b/primefaces/src/main/java/org/primefaces/component/tree/Tree.java
index 689948cbbd..f86533cc60 100644
--- a/primefaces/src/main/java/org/primefaces/component/tree/Tree.java
+++ b/primefaces/src/main/java/org/primefaces/component/tree/Tree.java
@@ -23,18 +23,6 @@
  */
 package org.primefaces.component.tree;
 
-import java.util.*;
-
-import javax.el.MethodExpression;
-import javax.faces.FacesException;
-import javax.faces.application.ResourceDependency;
-import javax.faces.component.UIComponent;
-import javax.faces.context.FacesContext;
-import javax.faces.event.AjaxBehaviorEvent;
-import javax.faces.event.BehaviorEvent;
-import javax.faces.event.FacesEvent;
-import javax.faces.event.PhaseId;
-
 import org.primefaces.PrimeFaces;
 import org.primefaces.event.*;
 import org.primefaces.model.CheckboxTreeNode;
@@ -46,6 +34,17 @@ import org.primefaces.util.ComponentUtils;
 import org.primefaces.util.Constants;
 import org.primefaces.util.MapBuilder;
 
+import javax.el.MethodExpression;
+import javax.faces.FacesException;
+import javax.faces.application.ResourceDependency;
+import javax.faces.component.UIComponent;
+import javax.faces.context.FacesContext;
+import javax.faces.event.AjaxBehaviorEvent;
+import javax.faces.event.BehaviorEvent;
+import javax.faces.event.FacesEvent;
+import javax.faces.event.PhaseId;
+import java.util.*;
+
 @ResourceDependency(library = "primefaces", name = "components.css")
 @ResourceDependency(library = "primefaces", name = "jquery/jquery.js")
 @ResourceDependency(library = "primefaces", name = "jquery/jquery-plugins.js")
@@ -352,7 +351,7 @@ public class Tree extends TreeBase {
         }
     }
 
-    public TreeNode createCopyOfTreeNode(TreeNode node) {
+    public TreeNode createCopyOfTreeNode(TreeNode<?> node) {
         TreeNode newNode;
         if (node instanceof CheckboxTreeNode) {
             newNode = new CheckboxTreeNode(node.getData());
diff --git a/primefaces/src/main/java/org/primefaces/component/tree/TreeRenderer.java b/primefaces/src/main/java/org/primefaces/component/tree/TreeRenderer.java
index 4ea7616ea0..390b00c405 100644
--- a/primefaces/src/main/java/org/primefaces/component/tree/TreeRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/tree/TreeRenderer.java
@@ -23,18 +23,6 @@
  */
 package org.primefaces.component.tree;
 
-import static org.primefaces.component.api.UITree.ROOT_ROW_KEY;
-
-import java.io.IOException;
-import java.util.*;
-
-import javax.el.ValueExpression;
-import javax.faces.FacesException;
-import javax.faces.component.UIComponent;
-import javax.faces.component.UINamingContainer;
-import javax.faces.context.FacesContext;
-import javax.faces.context.ResponseWriter;
-
 import org.primefaces.PrimeFaces;
 import org.primefaces.component.api.UITree;
 import org.primefaces.model.MatchMode;
@@ -45,6 +33,17 @@ import org.primefaces.renderkit.InputRenderer;
 import org.primefaces.renderkit.RendererUtils;
 import org.primefaces.util.*;
 
+import javax.el.ValueExpression;
+import javax.faces.FacesException;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UINamingContainer;
+import javax.faces.context.FacesContext;
+import javax.faces.context.ResponseWriter;
+import java.io.IOException;
+import java.util.*;
+
+import static org.primefaces.component.api.UITree.ROOT_ROW_KEY;
+
 public class TreeRenderer extends CoreRenderer {
 
     private static final String SB_DECODE_SELECTION = TreeRenderer.class.getName() + "#decodeSelection";
@@ -264,7 +263,7 @@ public class TreeRenderer extends CoreRenderer {
         }
     }
 
-    protected void encodeFilteredNodes(FacesContext context, Tree tree, TreeNode node, String filteredValue, Locale filterLocale)
+    protected void encodeFilteredNodes(FacesContext context, Tree tree, TreeNode<?> node, String filteredValue, Locale filterLocale)
             throws IOException {
         int childCount = node.getChildCount();
         if (childCount > 0) {
@@ -796,7 +795,7 @@ public class TreeRenderer extends CoreRenderer {
         }
     }
 
-    public void encodeTreeNodeChildren(FacesContext context, Tree tree, TreeNode root, TreeNode node, String clientId, boolean dynamic,
+    public void encodeTreeNodeChildren(FacesContext context, Tree tree, TreeNode<?> root, TreeNode<?> node, String clientId, boolean dynamic,
                                        boolean checkbox, boolean droppable) throws IOException {
 
         int childCount = node.getChildCount();
diff --git a/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableRenderer.java b/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableRenderer.java
index c989fb9fa6..35644a2670 100644
--- a/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/treetable/TreeTableRenderer.java
@@ -23,32 +23,6 @@
  */
 package org.primefaces.component.treetable;
 
-import static org.primefaces.component.api.UITree.ROOT_ROW_KEY;
-
-import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import javax.el.ELContext;
-import javax.faces.FacesException;
-import javax.faces.component.EditableValueHolder;
-import javax.faces.component.UIComponent;
-import javax.faces.component.UINamingContainer;
-import javax.faces.component.ValueHolder;
-import javax.faces.component.visit.VisitContext;
-import javax.faces.context.FacesContext;
-import javax.faces.context.ResponseWriter;
-
 import org.primefaces.PrimeFaces;
 import org.primefaces.component.api.DynamicColumn;
 import org.primefaces.component.api.UIColumn;
@@ -67,6 +41,27 @@ import org.primefaces.renderkit.RendererUtils;
 import org.primefaces.util.*;
 import org.primefaces.visit.ResetInputVisitCallback;
 
+import javax.el.ELContext;
+import javax.faces.FacesException;
+import javax.faces.component.EditableValueHolder;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UINamingContainer;
+import javax.faces.component.ValueHolder;
+import javax.faces.component.visit.VisitContext;
+import javax.faces.context.FacesContext;
+import javax.faces.context.ResponseWriter;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.primefaces.component.api.UITree.ROOT_ROW_KEY;
+
 public class TreeTableRenderer extends DataRenderer {
 
     private static final Logger LOGGER = Logger.getLogger(TreeTableRenderer.class.getName());
@@ -1336,7 +1331,7 @@ public class TreeTableRenderer extends DataRenderer {
         }
     }
 
-    protected void collectFilteredRowKeys(FacesContext context, TreeTable tt, TreeNode root, TreeNode node,  Map<String, FilterMeta> filterBy,
+    protected void collectFilteredRowKeys(FacesContext context, TreeTable tt, TreeNode<?> root, TreeNode<?> node,  Map<String, FilterMeta> filterBy,
             Locale filterLocale, List<String> filteredRowKeys) throws IOException {
 
         ELContext elContext = context.getELContext();
@@ -1398,7 +1393,7 @@ public class TreeTableRenderer extends DataRenderer {
         }
     }
 
-    private void createFilteredValueFromRowKeys(TreeTable tt, TreeNode node, TreeNode filteredNode, List<String> filteredRowKeys) {
+    private void createFilteredValueFromRowKeys(TreeTable tt, TreeNode<?> node, TreeNode<?> filteredNode, List<String> filteredRowKeys) {
         int childCount = node.getChildCount();
         for (int i = 0; i < childCount; i++) {
             TreeNode childNode = node.getChildren().get(i);
@@ -1418,7 +1413,7 @@ public class TreeTableRenderer extends DataRenderer {
         }
     }
 
-    protected TreeNode cloneTreeNode(TreeTable tt, TreeNode node, TreeNode parent) {
+    protected TreeNode cloneTreeNode(TreeTable tt, TreeNode<?> node, TreeNode<?> parent) {
         TreeNode clone = null;
 
         // equals check instead of instanceof to allow subclassing
diff --git a/primefaces/src/main/java/org/primefaces/component/treetable/export/TreeTableExporter.java b/primefaces/src/main/java/org/primefaces/component/treetable/export/TreeTableExporter.java
index e6caa163a6..b00eb7ced6 100644
--- a/primefaces/src/main/java/org/primefaces/component/treetable/export/TreeTableExporter.java
+++ b/primefaces/src/main/java/org/primefaces/component/treetable/export/TreeTableExporter.java
@@ -23,13 +23,12 @@
  */
 package org.primefaces.component.treetable.export;
 
-import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.reflect.Array;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
+import org.primefaces.component.export.ExportConfiguration;
+import org.primefaces.component.export.TableExporter;
+import org.primefaces.component.treetable.TreeTable;
+import org.primefaces.model.TreeNode;
+import org.primefaces.util.Constants;
+
 import javax.el.MethodExpression;
 import javax.faces.FacesException;
 import javax.faces.component.UIColumn;
@@ -38,12 +37,13 @@ import javax.faces.component.visit.VisitCallback;
 import javax.faces.component.visit.VisitContext;
 import javax.faces.component.visit.VisitResult;
 import javax.faces.context.FacesContext;
-
-import org.primefaces.component.export.ExportConfiguration;
-import org.primefaces.component.export.TableExporter;
-import org.primefaces.component.treetable.TreeTable;
-import org.primefaces.model.TreeNode;
-import org.primefaces.util.Constants;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 public abstract class TreeTableExporter extends TableExporter<TreeTable> {
 
@@ -256,7 +256,7 @@ public abstract class TreeTableExporter extends TableExporter<TreeTable> {
         return outputStream;
     }
 
-    protected static int getTreeRowCount(TreeNode node) {
+    protected static int getTreeRowCount(TreeNode<?> node) {
         int count = 1;
         if (node.getChildren() != null) {
             for (TreeNode childNode : node.getChildren()) {
@@ -278,7 +278,7 @@ public abstract class TreeTableExporter extends TableExporter<TreeTable> {
      * @param rowIndex
      * @return data of found treenode
      */
-    protected static Object traverseTree(TreeNode node, MutableInt rowIndex) {
+    protected static Object traverseTree(TreeNode<?> node, MutableInt rowIndex) {
 
         int index = rowIndex.getValue();
         rowIndex.decrement();
diff --git a/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNode.java b/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNode.java
index 5b4659c285..157c2752e0 100644
--- a/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNode.java
+++ b/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNode.java
@@ -26,7 +26,7 @@ package org.primefaces.model;
 import java.io.Serializable;
 import java.util.List;
 
-public class CheckboxTreeNode implements TreeNode, Serializable {
+public class CheckboxTreeNode<T> implements TreeNode<T>, Serializable {
 
     public static final String DEFAULT_TYPE = "default";
 
@@ -34,9 +34,9 @@ public class CheckboxTreeNode implements TreeNode, Serializable {
 
     private String type;
 
-    private Object data;
+    private T data;
 
-    private List<TreeNode> children;
+    private List<TreeNode<T>> children;
 
     private TreeNode parent;
 
@@ -55,13 +55,13 @@ public class CheckboxTreeNode implements TreeNode, Serializable {
         this.children = new CheckboxTreeNodeChildren(this);
     }
 
-    public CheckboxTreeNode(Object data) {
+    public CheckboxTreeNode(T data) {
         this.type = DEFAULT_TYPE;
         this.children = new CheckboxTreeNodeChildren(this);
         this.data = data;
     }
 
-    public CheckboxTreeNode(Object data, TreeNode parent) {
+    public CheckboxTreeNode(T data, TreeNode parent) {
         this.type = DEFAULT_TYPE;
         this.data = data;
         this.children = new CheckboxTreeNodeChildren(this);
@@ -70,7 +70,7 @@ public class CheckboxTreeNode implements TreeNode, Serializable {
         }
     }
 
-    public CheckboxTreeNode(String type, Object data, TreeNode parent) {
+    public CheckboxTreeNode(String type, T data, TreeNode parent) {
         this.type = type;
         this.data = data;
         this.children = new CheckboxTreeNodeChildren(this);
@@ -90,25 +90,25 @@ public class CheckboxTreeNode implements TreeNode, Serializable {
     }
 
     @Override
-    public Object getData() {
+    public T getData() {
         return data;
     }
 
-    public void setData(Object data) {
+    public void setData(T data) {
         this.data = data;
     }
 
     @Override
-    public List<TreeNode> getChildren() {
+    public List<TreeNode<T>> getChildren() {
         return children;
     }
 
-    public void setChildren(List<TreeNode> children) {
+    public void setChildren(List<TreeNode<T>> children) {
         if (children instanceof CheckboxTreeNodeChildren) {
             this.children = children;
         }
         else {
-            CheckboxTreeNodeChildren nodeChildren = new CheckboxTreeNodeChildren(this);
+            CheckboxTreeNodeChildren<T> nodeChildren = new CheckboxTreeNodeChildren<T>(this);
             nodeChildren.addAll(children);
             this.children = nodeChildren;
         }
diff --git a/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java b/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java
index 298d6c70cc..5effcbaa86 100644
--- a/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java
+++ b/primefaces/src/main/java/org/primefaces/model/CheckboxTreeNodeChildren.java
@@ -27,17 +27,17 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 
-public class CheckboxTreeNodeChildren extends TreeNodeList {
+public class CheckboxTreeNodeChildren<T> extends TreeNodeList<T> {
 
     private static final long serialVersionUID = 1L;
 
     private TreeNode parent;
 
-    public CheckboxTreeNodeChildren(TreeNode parent) {
+    public CheckboxTreeNodeChildren(TreeNode<T> parent) {
         this.parent = parent;
     }
 
-    private void eraseParent(TreeNode node) {
+    private void eraseParent(TreeNode<T> node) {
         TreeNode parentNode = node.getParent();
         if (parentNode != null) {
             parentNode.getChildren().remove(node);
@@ -46,7 +46,7 @@ public class CheckboxTreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public boolean add(TreeNode node) {
+    public boolean add(TreeNode<T> node) {
         if (node == null) {
             throw new NullPointerException();
         }
@@ -61,7 +61,7 @@ public class CheckboxTreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public void add(int index, TreeNode node) {
+    public void add(int index, TreeNode<T> node) {
         if (node == null) {
             throw new NullPointerException();
         }
@@ -78,7 +78,7 @@ public class CheckboxTreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public boolean addAll(Collection<? extends TreeNode> collection) {
+    public boolean addAll(Collection<? extends TreeNode<T>> collection) {
         Iterator<TreeNode> elements = (new ArrayList<TreeNode>(collection)).iterator();
         boolean changed = false;
         while (elements.hasNext()) {
@@ -103,7 +103,7 @@ public class CheckboxTreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public boolean addAll(int index, Collection<? extends TreeNode> collection) {
+    public boolean addAll(int index, Collection<? extends TreeNode<T>> collection) {
         Iterator<TreeNode> elements = (new ArrayList<TreeNode>(collection)).iterator();
         boolean changed = false;
         while (elements.hasNext()) {
@@ -210,7 +210,7 @@ public class CheckboxTreeNodeChildren extends TreeNodeList {
         }
     }
 
-    private void updateRowKeys(TreeNode node) {
+    private void updateRowKeys(TreeNode<?> node) {
         int childCount = node.getChildCount();
         if (childCount > 0) {
             for (int i = 0; i < childCount; i++) {
@@ -223,7 +223,7 @@ public class CheckboxTreeNodeChildren extends TreeNodeList {
         }
     }
 
-    private void updateSelectionState(TreeNode node) {
+    private void updateSelectionState(TreeNode<?> node) {
         boolean allChildrenSelected = true;
         boolean partialSelected = false;
 
diff --git a/primefaces/src/main/java/org/primefaces/model/DefaultTreeNode.java b/primefaces/src/main/java/org/primefaces/model/DefaultTreeNode.java
index 685b450590..93c7abc225 100644
--- a/primefaces/src/main/java/org/primefaces/model/DefaultTreeNode.java
+++ b/primefaces/src/main/java/org/primefaces/model/DefaultTreeNode.java
@@ -26,7 +26,7 @@ package org.primefaces.model;
 import java.io.Serializable;
 import java.util.List;
 
-public class DefaultTreeNode implements TreeNode, Serializable {
+public class DefaultTreeNode<T> implements TreeNode<T>, Serializable {
 
     public static final String DEFAULT_TYPE = "default";
 
@@ -35,9 +35,9 @@ public class DefaultTreeNode implements TreeNode, Serializable {
 
     private String type;
 
-    private Object data;
+    private T data;
 
-    private List<TreeNode> children;
+    private List<TreeNode<T>> children;
 
     private TreeNode parent;
 
@@ -51,16 +51,16 @@ public class DefaultTreeNode implements TreeNode, Serializable {
 
     public DefaultTreeNode() {
         this.type = DEFAULT_TYPE;
-        this.children = new TreeNodeChildren(this);
+        this.children = new TreeNodeChildren<T>(this);
     }
 
-    public DefaultTreeNode(Object data) {
+    public DefaultTreeNode(T data) {
         this.type = DEFAULT_TYPE;
         this.children = new TreeNodeChildren(this);
         this.data = data;
     }
 
-    public DefaultTreeNode(Object data, TreeNode parent) {
+    public DefaultTreeNode(T data, TreeNode parent) {
         this.type = DEFAULT_TYPE;
         this.data = data;
         this.children = new TreeNodeChildren(this);
@@ -69,7 +69,7 @@ public class DefaultTreeNode implements TreeNode, Serializable {
         }
     }
 
-    public DefaultTreeNode(String type, Object data, TreeNode parent) {
+    public DefaultTreeNode(String type, T data, TreeNode parent) {
         this.type = type;
         this.data = data;
         this.children = new TreeNodeChildren(this);
@@ -89,20 +89,20 @@ public class DefaultTreeNode implements TreeNode, Serializable {
     }
 
     @Override
-    public Object getData() {
+    public T getData() {
         return data;
     }
 
-    public void setData(Object data) {
+    public void setData(T data) {
         this.data = data;
     }
 
     @Override
-    public List<TreeNode> getChildren() {
+    public List<TreeNode<T>> getChildren() {
         return children;
     }
 
-    public void setChildren(List<TreeNode> children) {
+    public void setChildren(List<TreeNode<T>> children) {
         if (children instanceof TreeNodeChildren) {
             this.children = children;
         }
diff --git a/primefaces/src/main/java/org/primefaces/model/TreeNode.java b/primefaces/src/main/java/org/primefaces/model/TreeNode.java
index ebe95563df..36c0f54d34 100644
--- a/primefaces/src/main/java/org/primefaces/model/TreeNode.java
+++ b/primefaces/src/main/java/org/primefaces/model/TreeNode.java
@@ -25,19 +25,19 @@ package org.primefaces.model;
 
 import java.util.List;
 
-public interface TreeNode {
+public interface TreeNode<T> {
 
     String getType();
 
     void setType(String type);
 
-    Object getData();
+    T getData();
 
-    List<TreeNode> getChildren();
+    List<TreeNode<T>> getChildren();
 
-    TreeNode getParent();
+    TreeNode<T> getParent();
 
-    void setParent(TreeNode treeNode);
+    void setParent(TreeNode<T> treeNode);
 
     boolean isExpanded();
 
diff --git a/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java b/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java
index d4237ff43d..59e11638f9 100644
--- a/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java
+++ b/primefaces/src/main/java/org/primefaces/model/TreeNodeChildren.java
@@ -27,17 +27,17 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Iterator;
 
-public class TreeNodeChildren extends TreeNodeList {
+public class TreeNodeChildren<T> extends TreeNodeList<T> {
 
     private static final long serialVersionUID = 1L;
 
-    private TreeNode parent;
+    private TreeNode<T> parent;
 
-    public TreeNodeChildren(TreeNode parent) {
+    public TreeNodeChildren(TreeNode<T> parent) {
         this.parent = parent;
     }
 
-    private void eraseParent(TreeNode node) {
+    private void eraseParent(TreeNode<T> node) {
         TreeNode parentNode = node.getParent();
         if (parentNode != null) {
             parentNode.getChildren().remove(node);
@@ -46,7 +46,7 @@ public class TreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public boolean add(TreeNode node) {
+    public boolean add(TreeNode<T> node) {
         if (node == null) {
             throw new NullPointerException();
         }
@@ -60,7 +60,7 @@ public class TreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public void add(int index, TreeNode node) {
+    public void add(int index, TreeNode<T> node) {
         if (node == null) {
             throw new NullPointerException();
         }
@@ -76,8 +76,8 @@ public class TreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public boolean addAll(Collection<? extends TreeNode> collection) {
-        Iterator<TreeNode> elements = (new ArrayList<TreeNode>(collection)).iterator();
+    public boolean addAll(Collection<? extends TreeNode<T>> collection) {
+        Iterator<TreeNode<T>> elements = (new ArrayList<TreeNode<T>>(collection)).iterator();
         int index = collection.size();
         boolean changed = false;
         while (elements.hasNext()) {
@@ -101,8 +101,8 @@ public class TreeNodeChildren extends TreeNodeList {
     }
 
     @Override
-    public boolean addAll(int index, Collection<? extends TreeNode> collection) {
-        Iterator<TreeNode> elements = (new ArrayList<TreeNode>(collection)).iterator();
+    public boolean addAll(int index, Collection<? extends TreeNode<T>> collection) {
+        Iterator<TreeNode<T>> elements = (new ArrayList<TreeNode<T>>(collection)).iterator();
         boolean changed = false;
         while (elements.hasNext()) {
             TreeNode node = elements.next();
@@ -204,7 +204,7 @@ public class TreeNodeChildren extends TreeNodeList {
         }
     }
 
-    private void updateRowKeys(TreeNode node) {
+    private void updateRowKeys(TreeNode<?> node) {
         int childCount = node.getChildCount();
         if (childCount > 0) {
             for (int i = 0; i < childCount; i++) {
@@ -215,7 +215,7 @@ public class TreeNodeChildren extends TreeNodeList {
         }
     }
 
-    private void updateRowKeys(int index, TreeNode node) {
+    private void updateRowKeys(int index, TreeNode<?> node) {
         int childCount = node.getChildCount();
         if (childCount > 0) {
             for (int i = index; i < childCount; i++) {
@@ -225,7 +225,7 @@ public class TreeNodeChildren extends TreeNodeList {
         }
     }
 
-    private void updateRowKeys(TreeNode node, TreeNode childNode, int i) {
+    private void updateRowKeys(TreeNode<?> node, TreeNode<?> childNode, int i) {
         String childRowKey = node.getParent() == null ? String.valueOf(i) : node.getRowKey() + "_" + i;
         childNode.setRowKey(childRowKey);
         this.updateRowKeys(childNode);
diff --git a/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java b/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java
index 8d67bd2281..71425d6556 100644
--- a/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java
+++ b/primefaces/src/main/java/org/primefaces/model/TreeNodeList.java
@@ -25,7 +25,7 @@ package org.primefaces.model;
 
 import java.util.ArrayList;
 
-public class TreeNodeList extends ArrayList<TreeNode> {
+public class TreeNodeList<T> extends ArrayList<TreeNode<T>> {
 
     private static final long serialVersionUID = 1L;
 
@@ -36,7 +36,7 @@ public class TreeNodeList extends ArrayList<TreeNode> {
      * @param node node to be stored at the specified position
      * @return the node previously at the specified position
      */
-    public TreeNode setSibling(int index, TreeNode node) {
+    public TreeNode<T> setSibling(int index, TreeNode<T> node) {
         throw new UnsupportedOperationException();
     }
 }
diff --git a/primefaces/src/main/java/org/primefaces/util/TreeUtils.java b/primefaces/src/main/java/org/primefaces/util/TreeUtils.java
index bbc7770fcf..62ccf8a677 100644
--- a/primefaces/src/main/java/org/primefaces/util/TreeUtils.java
+++ b/primefaces/src/main/java/org/primefaces/util/TreeUtils.java
@@ -23,11 +23,12 @@
  */
 package org.primefaces.util;
 
-import java.util.Arrays;
-import java.util.Comparator;
 import org.primefaces.model.TreeNode;
 import org.primefaces.model.TreeNodeList;
 
+import java.util.Arrays;
+import java.util.Comparator;
+
 /**
  * Utilities for Tree components.
  */
@@ -42,8 +43,8 @@ public class TreeUtils {
      * @param node Node instance whose children to be sorted
      * @param comparator Comparator to use in sorting
      */
-    public static void sortNode(TreeNode node, Comparator comparator) {
-        TreeNodeList children = (TreeNodeList) node.getChildren();
+    public static void sortNode(TreeNode<?> node, Comparator comparator) {
+        TreeNodeList<?> children = (TreeNodeList) node.getChildren();
 
         if (children != null && !children.isEmpty()) {
             Object[] childrenArray = children.toArray();


Test patch:
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/Document.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/Document.java
new file mode 100644
index 0000000000..5da2514ecb
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/Document.java
@@ -0,0 +1,47 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString(onlyExplicitlyIncluded = true)
+public class Document implements Serializable, Comparable<Document> {
+
+    @ToString.Include
+    private String name;
+    private String size;
+    private String type;
+
+    public int compareTo(Document document) {
+        return this.getName().compareTo(document.getName());
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentService.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentService.java
new file mode 100644
index 0000000000..840ce9c889
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentService.java
@@ -0,0 +1,166 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.primefaces.model.CheckboxTreeNode;
+import org.primefaces.model.DefaultTreeNode;
+import org.primefaces.model.TreeNode;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Named;
+
+@Named
+@ApplicationScoped
+public class DocumentService {
+
+    public TreeNode createDocuments() {
+        TreeNode root = new DefaultTreeNode(new Document("Files", "-", "Folder"), null);
+
+        TreeNode applications = new DefaultTreeNode(new Document("Applications", "100kb", "Folder"), root);
+        TreeNode cloud = new DefaultTreeNode(new Document("Cloud", "20kb", "Folder"), root);
+        TreeNode desktop = new DefaultTreeNode(new Document("Desktop", "150kb", "Folder"), root);
+        TreeNode documents = new DefaultTreeNode(new Document("Documents", "75kb", "Folder"), root);
+        TreeNode downloads = new DefaultTreeNode(new Document("Downloads", "25kb", "Folder"), root);
+        TreeNode main = new DefaultTreeNode(new Document("Main", "50kb", "Folder"), root);
+        TreeNode other = new DefaultTreeNode(new Document("Other", "5kb", "Folder"), root);
+        TreeNode pictures = new DefaultTreeNode(new Document("Pictures", "150kb", "Folder"), root);
+        TreeNode videos = new DefaultTreeNode(new Document("Videos", "1500kb", "Folder"), root);
+
+        //Applications
+        TreeNode primeface = new DefaultTreeNode(new Document("Primefaces", "25kb", "Folder"), applications);
+        TreeNode primefacesapp = new DefaultTreeNode("app", new Document("primefaces.app", "10kb", "Application"), primeface);
+        TreeNode nativeapp = new DefaultTreeNode("app", new Document("native.app", "10kb", "Application"), primeface);
+        TreeNode mobileapp = new DefaultTreeNode("app", new Document("mobile.app", "5kb", "Application"), primeface);
+        TreeNode editorapp = new DefaultTreeNode("app", new Document("editor.app", "25kb", "Application"), applications);
+        TreeNode settingsapp = new DefaultTreeNode("app", new Document("settings.app", "50kb", "Application"), applications);
+
+        //Cloud
+        TreeNode backup1 = new DefaultTreeNode("document", new Document("backup-1.zip", "10kb", "Zip"), cloud);
+        TreeNode backup2 = new DefaultTreeNode("document", new Document("backup-2.zip", "10kb", "Zip"), cloud);
+
+        //Desktop
+        TreeNode note1 = new DefaultTreeNode("document", new Document("note-meeting.txt", "50kb", "Text"), desktop);
+        TreeNode note2 = new DefaultTreeNode("document", new Document("note-todo.txt", "100kb", "Text"), desktop);
+
+        //Documents
+        TreeNode work = new DefaultTreeNode(new Document("Work", "55kb", "Folder"), documents);
+        TreeNode expenses = new DefaultTreeNode("document", new Document("Expenses.doc", "30kb", "Document"), work);
+        TreeNode resume = new DefaultTreeNode("document", new Document("Resume.doc", "25kb", "Resume"), work);
+        TreeNode home = new DefaultTreeNode(new Document("Home", "20kb", "Folder"), documents);
+        TreeNode invoices = new DefaultTreeNode("excel", new Document("Invoice.xsl", "20kb", "Excel"), home);
+
+        //Downloads
+        TreeNode spanish = new DefaultTreeNode(new Document("Spanish", "10kb", "Folder"), downloads);
+        TreeNode tutorial1 = new DefaultTreeNode("document", new Document("tutorial-a1.txt", "5kb", "Text"), spanish);
+        TreeNode tutorial2 = new DefaultTreeNode("document", new Document("tutorial-a2.txt", "5kb", "Text"), spanish);
+        TreeNode travel = new DefaultTreeNode(new Document("Travel", "15kb", "Folder"), downloads);
+        TreeNode hotelpdf = new DefaultTreeNode("travel", new Document("Hotel.pdf", "10kb", "PDF"), travel);
+        TreeNode flightpdf = new DefaultTreeNode("travel", new Document("Flight.pdf", "5kb", "PDF"), travel);
+
+        //Main
+        TreeNode bin = new DefaultTreeNode("document", new Document("bin", "50kb", "Link"), main);
+        TreeNode etc = new DefaultTreeNode("document", new Document("etc", "100kb", "Link"), main);
+        TreeNode var = new DefaultTreeNode("document", new Document("var", "100kb", "Link"), main);
+
+        //Other
+        TreeNode todotxt = new DefaultTreeNode("document", new Document("todo.txt", "3kb", "Text"), other);
+        TreeNode logopng = new DefaultTreeNode("picture", new Document("logo.png", "2kb", "Picture"), other);
+
+        //Pictures
+        TreeNode barcelona = new DefaultTreeNode("picture", new Document("barcelona.jpg", "90kb", "Picture"), pictures);
+        TreeNode primeng = new DefaultTreeNode("picture", new Document("primefaces.png", "30kb", "Picture"), pictures);
+        TreeNode prime = new DefaultTreeNode("picture", new Document("prime.jpg", "30kb", "Picture"), pictures);
+
+        //Videos
+        TreeNode primefacesmkv = new DefaultTreeNode("video", new Document("primefaces.mkv", "1000kb", "Video"), videos);
+        TreeNode introavi = new DefaultTreeNode("video", new Document("intro.avi", "500kb", "Video"), videos);
+
+        return root;
+    }
+
+    public TreeNode createCheckboxDocuments() {
+        TreeNode root = new CheckboxTreeNode(new Document("Files", "-", "Folder"), null);
+
+        TreeNode applications = new CheckboxTreeNode(new Document("Applications", "100kb", "Folder"), root);
+        TreeNode cloud = new CheckboxTreeNode(new Document("Cloud", "20kb", "Folder"), root);
+        TreeNode desktop = new CheckboxTreeNode(new Document("Desktop", "150kb", "Folder"), root);
+        TreeNode documents = new CheckboxTreeNode(new Document("Documents", "75kb", "Folder"), root);
+        TreeNode downloads = new CheckboxTreeNode(new Document("Downloads", "25kb", "Folder"), root);
+        TreeNode main = new CheckboxTreeNode(new Document("Main", "50kb", "Folder"), root);
+        TreeNode other = new CheckboxTreeNode(new Document("Other", "5kb", "Folder"), root);
+        TreeNode pictures = new CheckboxTreeNode(new Document("Pictures", "150kb", "Folder"), root);
+        TreeNode videos = new CheckboxTreeNode(new Document("Videos", "1500kb", "Folder"), root);
+
+        //Applications
+        TreeNode primeface = new CheckboxTreeNode(new Document("Primefaces", "25kb", "Folder"), applications);
+        TreeNode primefacesapp = new CheckboxTreeNode("app", new Document("primefaces.app", "10kb", "Application"), primeface);
+        TreeNode nativeapp = new CheckboxTreeNode("app", new Document("native.app", "10kb", "Application"), primeface);
+        TreeNode mobileapp = new CheckboxTreeNode("app", new Document("mobile.app", "5kb", "Application"), primeface);
+        TreeNode editorapp = new CheckboxTreeNode("app", new Document("editor.app", "25kb", "Application"), applications);
+        TreeNode settingsapp = new CheckboxTreeNode("app", new Document("settings.app", "50kb", "Application"), applications);
+
+        //Cloud
+        TreeNode backup1 = new CheckboxTreeNode("document", new Document("backup-1.zip", "10kb", "Zip"), cloud);
+        TreeNode backup2 = new CheckboxTreeNode("document", new Document("backup-2.zip", "10kb", "Zip"), cloud);
+
+        //Desktop
+        TreeNode note1 = new CheckboxTreeNode("document", new Document("note-meeting.txt", "50kb", "Text"), desktop);
+        TreeNode note2 = new CheckboxTreeNode("document", new Document("note-todo.txt", "100kb", "Text"), desktop);
+
+        //Documents
+        TreeNode work = new CheckboxTreeNode(new Document("Work", "55kb", "Folder"), documents);
+        TreeNode expenses = new CheckboxTreeNode("document", new Document("Expenses.doc", "30kb", "Document"), work);
+        TreeNode resume = new CheckboxTreeNode("document", new Document("Resume.doc", "25kb", "Resume"), work);
+        TreeNode home = new CheckboxTreeNode(new Document("Home", "20kb", "Folder"), documents);
+        TreeNode invoices = new CheckboxTreeNode("excel", new Document("Invoice.xsl", "20kb", "Excel"), home);
+
+        //Downloads
+        TreeNode spanish = new CheckboxTreeNode(new Document("Spanish", "10kb", "Folder"), downloads);
+        TreeNode tutorial1 = new CheckboxTreeNode("document", new Document("tutorial-a1.txt", "5kb", "Text"), spanish);
+        TreeNode tutorial2 = new CheckboxTreeNode("document", new Document("tutorial-a2.txt", "5kb", "Text"), spanish);
+        TreeNode travel = new CheckboxTreeNode(new Document("Travel", "15kb", "Folder"), downloads);
+        TreeNode hotelpdf = new CheckboxTreeNode("travel", new Document("Hotel.pdf", "10kb", "PDF"), travel);
+        TreeNode flightpdf = new CheckboxTreeNode("travel", new Document("Flight.pdf", "5kb", "PDF"), travel);
+
+        //Main
+        TreeNode bin = new CheckboxTreeNode("document", new Document("bin", "50kb", "Link"), main);
+        TreeNode etc = new CheckboxTreeNode("document", new Document("etc", "100kb", "Link"), main);
+        TreeNode var = new CheckboxTreeNode("document", new Document("var", "100kb", "Link"), main);
+
+        //Other
+        TreeNode todotxt = new CheckboxTreeNode("document", new Document("todo.txt", "3kb", "Text"), other);
+        TreeNode logopng = new CheckboxTreeNode("picture", new Document("logo.png", "2kb", "Picture"), other);
+
+        //Pictures
+        TreeNode barcelona = new CheckboxTreeNode("picture", new Document("barcelona.jpg", "90kb", "Picture"), pictures);
+        TreeNode primeng = new CheckboxTreeNode("picture", new Document("primefaces.png", "30kb", "Picture"), pictures);
+        TreeNode prime = new CheckboxTreeNode("picture", new Document("prime.jpg", "30kb", "Picture"), pictures);
+
+        //Videos
+        TreeNode primefacesmkv = new CheckboxTreeNode("video", new Document("primefaces.mkv", "1000kb", "Video"), videos);
+        TreeNode introavi = new CheckboxTreeNode("video", new Document("intro.avi", "500kb", "Video"), videos);
+
+        return root;
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentSorterNameAscSizeDesc.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentSorterNameAscSizeDesc.java
new file mode 100644
index 0000000000..0b0cc70759
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentSorterNameAscSizeDesc.java
@@ -0,0 +1,49 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.primefaces.model.TreeNode;
+
+import java.util.Comparator;
+
+public class DocumentSorterNameAscSizeDesc implements Comparator<TreeNode<Document>> {
+
+    @Override
+    public int compare(TreeNode<Document> treeNodeDoc1, TreeNode<Document> treeNodeDoc2) {
+        Document doc1 = treeNodeDoc1.getData();
+        Document doc2 = treeNodeDoc2.getData();
+
+        try {
+            if (doc1.getName().equalsIgnoreCase(doc2.getName())) {
+                return doc2.getSize().compareToIgnoreCase(doc1.getSize());
+            }
+            else {
+                return doc1.getName().compareToIgnoreCase(doc2.getName());
+            }
+        }
+        catch (Exception e) {
+            throw new RuntimeException();
+        }
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentSorterSizeDescNameAsc.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentSorterSizeDescNameAsc.java
new file mode 100644
index 0000000000..88ebe5fc86
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/DocumentSorterSizeDescNameAsc.java
@@ -0,0 +1,49 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.primefaces.model.TreeNode;
+
+import java.util.Comparator;
+
+public class DocumentSorterSizeDescNameAsc implements Comparator<TreeNode<Document>> {
+
+    @Override
+    public int compare(TreeNode<Document> treeNodeDoc1, TreeNode<Document> treeNodeDoc2) {
+        Document doc1 = treeNodeDoc1.getData();
+        Document doc2 = treeNodeDoc2.getData();
+
+        try {
+            if (doc1.getSize().equalsIgnoreCase(doc2.getSize())) {
+                return doc1.getName().compareToIgnoreCase(doc2.getName());
+            }
+            else {
+                return doc2.getSize().compareToIgnoreCase(doc1.getSize());
+            }
+        }
+        catch (Exception e) {
+            throw new RuntimeException();
+        }
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable001.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable001.java
new file mode 100644
index 0000000000..fb660f7b6a
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable001.java
@@ -0,0 +1,82 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import lombok.Data;
+import org.primefaces.component.treetable.TreeTable;
+import org.primefaces.event.NodeSelectEvent;
+import org.primefaces.event.NodeUnselectEvent;
+import org.primefaces.integrationtests.general.utilities.TestUtils;
+import org.primefaces.model.TreeNode;
+
+import javax.annotation.PostConstruct;
+import javax.faces.context.FacesContext;
+import javax.faces.view.ViewScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.Serializable;
+
+@Named
+@ViewScoped
+@Data
+public class TreeTable001 implements Serializable {
+
+    private static final long serialVersionUID = 4336050122274677022L;
+
+    private TreeNode<Document> root;
+    private TreeNode<Document> selectedNode;
+
+    private Document selectedDocument;
+
+    @Inject
+    private DocumentService service;
+
+    @PostConstruct
+    public void init() {
+        root = service.createDocuments();
+    }
+
+    public void resetTable() {
+        TreeTable treeTable = (TreeTable) FacesContext.getCurrentInstance().getViewRoot().findComponent("form:treeTable");
+        treeTable.reset();
+
+        init();
+    }
+
+    public void showSelectedDocument() {
+        TestUtils.addMessage("selected document", selectedDocument.getName());
+    }
+
+    public void showSelectedNode() {
+        TestUtils.addMessage("selected node", selectedNode.getData().getName());
+    }
+
+    public void selectNode(NodeSelectEvent event) {
+        TestUtils.addMessage("select-event", ((Document) event.getTreeNode().getData()).getName());
+    }
+
+    public void unselectNode(NodeUnselectEvent event) {
+        TestUtils.addMessage("unselect-event", ((Document) event.getTreeNode().getData()).getName());
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable002.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable002.java
new file mode 100644
index 0000000000..cfdb39d62c
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable002.java
@@ -0,0 +1,56 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import lombok.Data;
+import org.primefaces.model.SortMeta;
+import org.primefaces.model.TreeNode;
+
+import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.Serializable;
+import java.util.List;
+
+@Named
+@ViewScoped
+@Data
+public class TreeTable002 implements Serializable {
+
+    private static final long serialVersionUID = -3735722784072615486L;
+
+    private TreeNode root;
+    private List<SortMeta> sortBy;
+
+    private Document selectedDocument;
+
+    @Inject
+    private DocumentService service;
+
+    @PostConstruct
+    public void init() {
+        root = service.createDocuments();
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable003.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable003.java
new file mode 100644
index 0000000000..8fdbd5b630
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable003.java
@@ -0,0 +1,53 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import lombok.Data;
+import org.primefaces.model.TreeNode;
+
+import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.Serializable;
+
+@Named
+@ViewScoped
+@Data
+public class TreeTable003 implements Serializable {
+
+    private static final long serialVersionUID = -7938825859167766622L;
+
+    private TreeNode root;
+
+    private Document selectedDocument;
+
+    @Inject
+    private DocumentService service;
+
+    @PostConstruct
+    public void init() {
+        root = service.createDocuments();
+    }
+}
diff --git a/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable004.java b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable004.java
new file mode 100644
index 0000000000..f6ca733e3a
--- /dev/null
+++ b/primefaces-integration-tests/src/main/java/org/primefaces/integrationtests/treetable/TreeTable004.java
@@ -0,0 +1,63 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import lombok.Data;
+import org.primefaces.integrationtests.general.utilities.TestUtils;
+import org.primefaces.model.TreeNode;
+
+import javax.annotation.PostConstruct;
+import javax.faces.view.ViewScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+@Named
+@ViewScoped
+@Data
+public class TreeTable004 implements Serializable {
+
+    private static final long serialVersionUID = -3049074201208091926L;
+
+    private TreeNode<Document> root;
+    private TreeNode<Document>[] selectedNodes;
+
+    @Inject
+    private DocumentService service;
+
+    @PostConstruct
+    public void init() {
+        root = service.createDocuments();
+    }
+
+    public void showSelectedNodes() {
+        if (selectedNodes != null) {
+            String selectedNodesStr = Arrays.stream(selectedNodes).map(n -> n.getData().getName()).collect(Collectors.joining(","));
+            TestUtils.addMessage("selected nodes", selectedNodesStr);
+        }
+    }
+
+}
diff --git a/primefaces-integration-tests/src/main/webapp/treetable/treeTable001.xhtml b/primefaces-integration-tests/src/main/webapp/treetable/treeTable001.xhtml
new file mode 100644
index 0000000000..2d7c34fecb
--- /dev/null
+++ b/primefaces-integration-tests/src/main/webapp/treetable/treeTable001.xhtml
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:h="http://xmlns.jcp.org/jsf/html"
+      xmlns:f="http://xmlns.jcp.org/jsf/core"
+      xmlns:p="http://primefaces.org/ui">
+
+<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
+    <h:head>
+
+    </h:head>
+
+    <h:body>
+
+        <h:form id="form">
+            <p:messages id="msgs" showDetail="true">
+                <p:autoUpdate/>
+            </p:messages>
+
+            <p:treeTable value="#{treeTable001.root}" var="document" id="treeTable" widgetVar="treeTable"
+                         sortMode="single" allowUnsorting="true" selectionMode="single"
+                         selection="#{treeTable001.selectedNode}">
+                <f:facet name="header">
+                    <div class="p-d-flex p-ai-center p-jc-between">
+                        <span>Document Viewer</span>
+                        <p:inputText id="globalFilter" onchange="PF('treeTable').filter()" style="width:10rem" placeholder="Enter keyword" />
+                    </div>
+                </f:facet>
+                <p:column headerText="Name" sortBy="#{document.name}" filterBy="#{document.name}" filterMatchMode="contains">
+                    <h:outputText value="#{document.name}"/>
+                </p:column>
+                <p:column headerText="Size" sortBy="#{document.size}">
+                    <h:outputText value="#{document.size}"/>
+                </p:column>
+                <p:column headerText="Type" sortBy="#{document.type}" filterBy="#{document.type}" filterMatchMode="exact">
+                    <f:facet name="filter">
+                        <p:selectOneMenu onchange="PF('treeTable').filter()" styleClass="custom-filter">
+                            <f:selectItem itemLabel="Select One" itemValue="#{null}" noSelectionOption="true"/>
+                            <f:selectItem itemLabel="Folder" itemValue="Folder"/>
+                            <f:selectItem itemLabel="Application" itemValue="Application"/>
+                            <f:selectItem itemLabel="Zip" itemValue="Zip"/>
+                            <f:selectItem itemLabel="Text" itemValue="Text"/>
+                            <f:selectItem itemLabel="Document" itemValue="Document"/>
+                            <f:selectItem itemLabel="Resume" itemValue="Resume"/>
+                            <f:selectItem itemLabel="Excel" itemValue="Excel"/>
+                            <f:selectItem itemLabel="PDF" itemValue="PDF"/>
+                            <f:selectItem itemLabel="Link" itemValue="Link"/>
+                            <f:selectItem itemLabel="Picture" itemValue="Picture"/>
+                            <f:selectItem itemLabel="Video" itemValue="Video"/>
+                        </p:selectOneMenu>
+                    </f:facet>
+
+                    <h:outputText value="#{document.type}"/>
+                </p:column>
+                <p:column style="width:3rem">
+                    <p:commandButton title="View Detail" icon="pi pi-search" action="#{treeTable001.showSelectedDocument}">
+                        <f:setPropertyActionListener value="#{document}" target="#{treeTable001.selectedDocument}"/>
+                    </p:commandButton>
+                </p:column>
+
+                <p:ajax event="select" listener="#{treeTable001.selectNode}" />
+                <p:ajax event="unselect" listener="#{treeTable001.unselectNode}" />
+            </p:treeTable>
+
+            <p:commandButton id="button" value="Submit" update="@form"/>
+            <p:commandButton id="buttonUpdate" value="Update" update="treeTable"/>
+            <p:commandButton id="buttonResetTable" value="Reset" update="@form" action="#{treeTable001.resetTable()}" />
+            <p:commandButton id="buttonShowSelectedNode" value="show selected node" action="#{treeTable001.showSelectedNode}" />
+        </h:form>
+
+    </h:body>
+</f:view>
+
+</html>
diff --git a/primefaces-integration-tests/src/main/webapp/treetable/treeTable002.xhtml b/primefaces-integration-tests/src/main/webapp/treetable/treeTable002.xhtml
new file mode 100644
index 0000000000..57cd0a3840
--- /dev/null
+++ b/primefaces-integration-tests/src/main/webapp/treetable/treeTable002.xhtml
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:h="http://xmlns.jcp.org/jsf/html"
+      xmlns:f="http://xmlns.jcp.org/jsf/core"
+      xmlns:p="http://primefaces.org/ui">
+
+<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
+    <h:head>
+
+    </h:head>
+
+    <h:body>
+
+        <h:form id="form">
+            <p:messages id="msgs" showDetail="true">
+                <p:autoUpdate/>
+            </p:messages>
+
+            <p:treeTable value="#{treeTable002.root}" var="document" id="treetable" widgetVar="wgtTreeTable">
+                <f:facet name="header">
+                    Document Viewer
+                </f:facet>
+                <p:column headerText="Name" sortBy="#{document.name}" sortOrder="asc" sortPriority="1">
+                    <h:outputText value="#{document.name}"/>
+                </p:column>
+                <p:column headerText="Size" sortBy="#{document.size}" sortOrder="desc" sortPriority="2">
+                    <h:outputText value="#{document.size}"/>
+                </p:column>
+                <p:column headerText="Type" sortBy="#{document.type}">
+                    <h:outputText value="#{document.type}"/>
+                </p:column>
+                <p:column style="width:3rem">
+                    <p:commandButton update="form:msgs"
+                                     title="View Detail" icon="pi pi-search">
+                        <f:setPropertyActionListener value="#{document}" target="#{treeTable002.selectedDocument}"/>
+                    </p:commandButton>
+                </p:column>
+            </p:treeTable>
+        </h:form>
+
+    </h:body>
+</f:view>
+
+</html>
diff --git a/primefaces-integration-tests/src/main/webapp/treetable/treeTable003.xhtml b/primefaces-integration-tests/src/main/webapp/treetable/treeTable003.xhtml
new file mode 100644
index 0000000000..31880fa59b
--- /dev/null
+++ b/primefaces-integration-tests/src/main/webapp/treetable/treeTable003.xhtml
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:h="http://xmlns.jcp.org/jsf/html"
+      xmlns:f="http://xmlns.jcp.org/jsf/core"
+      xmlns:p="http://primefaces.org/ui">
+
+<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
+    <h:head>
+
+    </h:head>
+
+    <h:body>
+
+        <h:form id="form">
+            <p:messages id="msgs" showDetail="true">
+                <p:autoUpdate/>
+            </p:messages>
+
+            <p:treeTable value="#{treeTable001.root}" var="document" id="treetable" widgetVar="wgtTreeTable"
+                         sortMode="single" allowUnsorting="true" paginator="true" rows="3">
+                <f:facet name="header">
+                    Document Viewer
+                </f:facet>
+                <p:column headerText="Name" sortBy="#{document.name}">
+                    <h:outputText value="#{document.name}"/>
+                </p:column>
+                <p:column headerText="Size" sortBy="#{document.size}">
+                    <h:outputText value="#{document.size}"/>
+                </p:column>
+                <p:column headerText="Type" sortBy="#{document.type}">
+                    <h:outputText value="#{document.type}"/>
+                </p:column>
+                <p:column style="width:3rem">
+                    <p:commandButton update="form:msgs"
+                                     title="View Detail" icon="pi pi-search">
+                        <f:setPropertyActionListener value="#{document}" target="#{treeTable001.selectedDocument}"/>
+                    </p:commandButton>
+                </p:column>
+            </p:treeTable>
+        </h:form>
+
+    </h:body>
+</f:view>
+
+</html>
diff --git a/primefaces-integration-tests/src/main/webapp/treetable/treeTable004.xhtml b/primefaces-integration-tests/src/main/webapp/treetable/treeTable004.xhtml
new file mode 100644
index 0000000000..992a0395c2
--- /dev/null
+++ b/primefaces-integration-tests/src/main/webapp/treetable/treeTable004.xhtml
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:h="http://xmlns.jcp.org/jsf/html"
+      xmlns:f="http://xmlns.jcp.org/jsf/core"
+      xmlns:p="http://primefaces.org/ui">
+
+<f:view contentType="text/html;charset=UTF-8" encoding="UTF-8">
+    <h:head>
+
+    </h:head>
+
+    <h:body>
+
+        <h:form id="form">
+            <p:messages id="msgs" showDetail="true">
+                <p:autoUpdate/>
+            </p:messages>
+
+            <p:treeTable value="#{treeTable004.root}" var="document" id="treeTable" widgetVar="treeTable"
+                         sortMode="single" allowUnsorting="true" selectionMode="multiple"
+                         selection="#{treeTable004.selectedNodes}">
+                <f:facet name="header">
+                    <div class="p-d-flex p-ai-center p-jc-between">
+                        <span>Document Viewer</span>
+                        <p:inputText id="globalFilter" onchange="PF('treeTable').filter()" style="width:10rem" placeholder="Enter keyword" />
+                    </div>
+                </f:facet>
+                <p:column headerText="Name" sortBy="#{document.name}" filterBy="#{document.name}" filterMatchMode="contains">
+                    <h:outputText value="#{document.name}"/>
+                </p:column>
+                <p:column headerText="Size" sortBy="#{document.size}">
+                    <h:outputText value="#{document.size}"/>
+                </p:column>
+                <p:column headerText="Type" sortBy="#{document.type}" filterBy="#{document.type}" filterMatchMode="exact">
+                    <f:facet name="filter">
+                        <p:selectOneMenu onchange="PF('treeTable').filter()" styleClass="custom-filter">
+                            <f:selectItem itemLabel="Select One" itemValue="#{null}" noSelectionOption="true"/>
+                            <f:selectItem itemLabel="Folder" itemValue="Folder"/>
+                            <f:selectItem itemLabel="Application" itemValue="Application"/>
+                            <f:selectItem itemLabel="Zip" itemValue="Zip"/>
+                            <f:selectItem itemLabel="Text" itemValue="Text"/>
+                            <f:selectItem itemLabel="Document" itemValue="Document"/>
+                            <f:selectItem itemLabel="Resume" itemValue="Resume"/>
+                            <f:selectItem itemLabel="Excel" itemValue="Excel"/>
+                            <f:selectItem itemLabel="PDF" itemValue="PDF"/>
+                            <f:selectItem itemLabel="Link" itemValue="Link"/>
+                            <f:selectItem itemLabel="Picture" itemValue="Picture"/>
+                            <f:selectItem itemLabel="Video" itemValue="Video"/>
+                        </p:selectOneMenu>
+                    </f:facet>
+
+                    <h:outputText value="#{document.type}"/>
+                </p:column>
+            </p:treeTable>
+
+            <p:commandButton id="button" value="Submit" update="@form"/>
+            <p:commandButton id="buttonUpdate" value="Update" update="treeTable"/>
+            <p:commandButton id="buttonResetTable" value="Reset" update="@form" action="#{treeTable004.resetTable()}" />
+            <p:commandButton id="buttonShowSelectedNodes" value="show selected nodse" action="#{treeTable004.showSelectedNodes}" />
+        </h:form>
+
+    </h:body>
+</f:view>
+
+</html>
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/AbstractTableTest.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/AbstractTableTest.java
new file mode 100644
index 0000000000..43f1a175a5
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/AbstractTableTest.java
@@ -0,0 +1,63 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests;
+
+import org.junit.jupiter.api.Assertions;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.primefaces.selenium.AbstractPrimePageTest;
+import org.primefaces.selenium.PrimeSelenium;
+
+public class AbstractTableTest extends AbstractPrimePageTest {
+
+    protected void assertHeaderSorted(WebElement header, String sortDirection, int sortPriority) {
+        String directionClass = null;
+        switch (sortDirection) {
+            case "ASC":
+                directionClass = "ui-icon-triangle-1-n";
+                break;
+            case "DESC":
+                directionClass = "ui-icon-triangle-1-s";
+                break;
+            default:
+                break;
+        }
+        Assertions.assertTrue(PrimeSelenium.hasCssClass(header.findElement(By.className("ui-sortable-column-icon")), directionClass));
+        WebElement badge = header.findElement(By.className("ui-sortable-column-badge"));
+        if (sortPriority > 0) {
+            Assertions.assertEquals(sortPriority, Integer.parseInt(badge.getText()));
+        }
+        else {
+            Assertions.assertEquals("", badge.getText());
+        }
+    }
+
+    protected void filterGlobal(WebElement inputGlobalFilter, String filter) {
+        // maybe we can move some of this to PF Selenium (InputText?, DataTable?)
+        inputGlobalFilter.clear();
+        inputGlobalFilter.sendKeys(filter);
+        PrimeSelenium.guardAjax(inputGlobalFilter).sendKeys(Keys.TAB);
+    }
+}
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/datatable/AbstractDataTableTest.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/datatable/AbstractDataTableTest.java
index 3444e3c345..a0201e4733 100644
--- a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/datatable/AbstractDataTableTest.java
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/datatable/AbstractDataTableTest.java
@@ -23,21 +23,17 @@
  */
 package org.primefaces.integrationtests.datatable;
 
-import java.util.Comparator;
-import java.util.List;
-import java.util.stream.Collectors;
-
 import org.junit.jupiter.api.Assertions;
-import org.openqa.selenium.By;
 import org.openqa.selenium.HasCapabilities;
-import org.openqa.selenium.Keys;
-import org.openqa.selenium.WebElement;
-import org.primefaces.selenium.AbstractPrimePageTest;
-import org.primefaces.selenium.PrimeSelenium;
+import org.primefaces.integrationtests.AbstractTableTest;
 import org.primefaces.selenium.component.DataTable;
 import org.primefaces.selenium.component.model.datatable.Row;
 
-public abstract class AbstractDataTableTest extends AbstractPrimePageTest {
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public abstract class AbstractDataTableTest extends AbstractTableTest {
 
     protected final List<ProgrammingLanguage> languages = new ProgrammingLanguageService().getLangs();
     protected final ProgrammingLanguageLazyDataModel model = new ProgrammingLanguageLazyDataModel();
@@ -102,28 +98,6 @@ public abstract class AbstractDataTableTest extends AbstractPrimePageTest {
         }
     }
 
-    protected void assertHeaderSorted(WebElement header, String sortDirection, int sortPriority) {
-        String directionClass = null;
-        switch (sortDirection) {
-            case "ASC":
-                directionClass = "ui-icon-triangle-1-n";
-                break;
-            case "DESC":
-                directionClass = "ui-icon-triangle-1-s";
-                break;
-            default:
-                break;
-        }
-        Assertions.assertTrue(PrimeSelenium.hasCssClass(header.findElement(By.className("ui-sortable-column-icon")), directionClass));
-        WebElement badge = header.findElement(By.className("ui-sortable-column-badge"));
-        if (sortPriority > 0) {
-            Assertions.assertEquals(sortPriority, Integer.parseInt(badge.getText()));
-        }
-        else {
-            Assertions.assertEquals("", badge.getText());
-        }
-    }
-
     protected void logWebDriverCapabilites(DataTable003Test.Page page) {
         if (page.getWebDriver() instanceof HasCapabilities) {
             HasCapabilities hasCaps = (HasCapabilities) page.getWebDriver();
@@ -136,10 +110,4 @@ public abstract class AbstractDataTableTest extends AbstractPrimePageTest {
         }
     }
 
-    protected void filterGlobal(WebElement inputGlobalFilter, String filter) {
-        // maybe we can move some of this to PF Selenium (InputText?, DataTable?)
-        inputGlobalFilter.clear();
-        inputGlobalFilter.sendKeys(filter);
-        PrimeSelenium.guardAjax(inputGlobalFilter).sendKeys(Keys.TAB);
-    }
 }
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/AbstractTreeTableTest.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/AbstractTreeTableTest.java
new file mode 100644
index 0000000000..18caf48327
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/AbstractTreeTableTest.java
@@ -0,0 +1,116 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.junit.jupiter.api.Assertions;
+import org.primefaces.integrationtests.AbstractTableTest;
+import org.primefaces.model.DefaultTreeNode;
+import org.primefaces.model.TreeNode;
+import org.primefaces.model.TreeNodeList;
+import org.primefaces.selenium.component.Messages;
+import org.primefaces.selenium.component.model.Msg;
+import org.primefaces.selenium.component.model.TreeTable;
+import org.primefaces.selenium.component.model.treetable.Row;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class AbstractTreeTableTest extends AbstractTableTest {
+
+    protected TreeNode<Document> root = new DocumentService().createDocuments();
+
+    protected void assertRows(TreeTable treeTable, TreeNode root) {
+        List<Row> rows = treeTable.getRows();
+        assertRows(rows, root);
+    }
+
+    protected void assertRows(List<Row> rows, TreeNode<Document> root) {
+        int expectedSize = countRowsRecursive(root);
+        Assertions.assertNotNull(rows);
+        if (expectedSize == 0) {
+            Assertions.assertEquals(1, rows.size()); // No records found.
+            return;
+        }
+        Assertions.assertEquals(expectedSize, rows.size());
+
+        int row = 0;
+        List<TreeNode<Document>> treeNodes = flattenTreeNodes(root);
+        for (TreeNode<Document> treeNode: treeNodes) {
+            Assertions.assertEquals(treeNode.getData().getName(), rows.get(row).getCell(0).getText());
+            Assertions.assertEquals(treeNode.getData().getSize(), rows.get(row).getCell(1).getText());
+            Assertions.assertEquals(treeNode.getData().getType(), rows.get(row).getCell(2).getText());
+            row++;
+        }
+    }
+
+    private int countRowsRecursive(TreeNode<Document> root) {
+        int cnt = root.getChildCount();
+        for (TreeNode<Document> child : root.getChildren()) {
+            if (child.isExpanded()) {
+                cnt += countRowsRecursive(child);
+            }
+        }
+        return cnt;
+    }
+
+    private List<TreeNode<Document>> flattenTreeNodes(TreeNode<Document> root) {
+        List<TreeNode<Document>> treeNodes = new TreeNodeList<>();
+        for (TreeNode<Document> child : root.getChildren()) {
+            treeNodes.add(child);
+            if (child.isExpanded()) {
+                treeNodes.addAll(flattenTreeNodes(child));
+            }
+        }
+        return treeNodes;
+    }
+
+    public TreeNode<Document> sortBy(TreeNode<Document> root, final Comparator comparator) {
+        List<TreeNode<Document>> listSorted = (List<TreeNode<Document>>) root.getChildren().stream()
+                .sorted(comparator)
+                .collect(Collectors.toList());
+
+        List<TreeNode<Document>> listSortedRecursive = new ArrayList<>();
+        for (TreeNode<Document> t: listSorted) {
+            if (t.getChildCount() > 0) {
+                listSortedRecursive.add(sortBy(t, comparator));
+            }
+            else {
+                listSortedRecursive.add(t);
+            }
+        }
+
+        DefaultTreeNode<Document> treeSorted = new DefaultTreeNode<>(root.getData(), null);
+        treeSorted.setChildren(listSortedRecursive);
+        treeSorted.setExpanded(root.isExpanded());
+        return treeSorted;
+    }
+
+    protected void assertMessage(Messages messages, int index, String summary, String detail) {
+        Msg message = messages.getMessage(index);
+        Assertions.assertEquals(summary, message.getSummary());
+        Assertions.assertEquals(detail, message.getDetail());
+    }
+}
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable001Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable001Test.java
new file mode 100644
index 0000000000..44cddfa2ef
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable001Test.java
@@ -0,0 +1,361 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Action;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.support.FindBy;
+import org.primefaces.model.TreeNode;
+import org.primefaces.selenium.AbstractPrimePage;
+import org.primefaces.selenium.PrimeSelenium;
+import org.primefaces.selenium.component.CommandButton;
+import org.primefaces.selenium.component.InputText;
+import org.primefaces.selenium.component.Messages;
+import org.primefaces.selenium.component.model.TreeTable;
+import org.primefaces.selenium.component.model.datatable.Header;
+import org.primefaces.selenium.component.model.treetable.Row;
+
+import java.util.Comparator;
+import java.util.List;
+
+public class TreeTable001Test extends AbstractTreeTableTest {
+
+    @Test
+    @Order(1)
+    @DisplayName("TreeTable: Basic & Toggling")
+    public void testBasicAndToggling(Page page) {
+        // Arrange
+        TreeTable treeTable = page.treeTable;
+        Assertions.assertNotNull(treeTable);
+
+        // Assert
+        Assertions.assertNotNull(treeTable.getHeaderWebElement());
+
+        List<WebElement> rowElts = treeTable.getRowsWebElement();
+        Assertions.assertNotNull(rowElts);
+        Assertions.assertEquals(root.getChildCount(), rowElts.size());
+
+        List<Row> rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(root.getChildCount(), rows.size());
+
+        assertRows(treeTable, root);
+
+        Header header = treeTable.getHeader();
+        Assertions.assertNotNull(header);
+        Assertions.assertNotNull(header.getCells());
+        Assertions.assertEquals(4, header.getCells().size());
+        Assertions.assertEquals("Name", header.getCell(0).getColumnTitle().getText());
+        Assertions.assertEquals("Size", header.getCell(1).getColumnTitle().getText());
+        Assertions.assertEquals("Type", header.getCell(2).getColumnTitle().getText());
+
+        Row firstRow = treeTable.getRow(0);
+        Assertions.assertTrue(firstRow.isToggleable());
+        Assertions.assertEquals(1, firstRow.getLevel());
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+
+        // Act
+        firstRow.toggle();
+
+        // Assert - Part 2
+        rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(root.getChildCount() + root.getChildren().get(0).getChildCount(), rows.size());
+
+        Row firstChildRow = treeTable.getRow(1);
+        Assertions.assertEquals(2, firstChildRow.getLevel());
+
+        root.getChildren().get(0).setExpanded(true);
+        assertRows(treeTable, root);
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+
+        // Act
+        firstRow.toggle();
+
+        // Assert - Part 3
+        rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(root.getChildCount(), rows.size());
+
+        root.getChildren().get(0).setExpanded(false);
+        assertRows(treeTable, root);
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(2)
+    @DisplayName("TreeTable: single sort including recursive subtree sorting")
+    public void testSortSingle(Page page) {
+        // Arrange
+        TreeTable treeTable = page.treeTable;
+        Assertions.assertNotNull(treeTable);
+
+        treeTable.getRow(0).toggle();
+        root.getChildren().get(0).setExpanded(true);
+
+        // Act - ascending
+        treeTable.sort("Name");
+
+        // Assert
+        TreeNode<Document> treeSorted = sortBy(root, Comparator.comparing((TreeNode<Document> t) -> t.getData().getName().toLowerCase()));
+        assertRows(treeTable, treeSorted);
+
+        // Act - descending
+        treeTable.sort("Name");
+
+        // Assert
+        TreeNode<Document> treeSortedRev = sortBy(treeSorted, Comparator.comparing((TreeNode<Document> t) -> t.getData().getName().toLowerCase()).reversed());
+        assertRows(treeTable, treeSortedRev);
+
+        // Act
+        page.buttonUpdate.click();
+
+        // Assert - sort must not be lost after update
+        assertRows(treeTable, treeSortedRev);
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(3)
+    @DisplayName("TreeTable: filter")
+    public void testFilter(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act (filter on L3)
+        treeTable.filter("Name", "mobi");
+
+        // Assert
+        // we try to avoid duplicating logic from TreeTableRenderer#filter, so we take the simple/stupid way
+        List<Row> rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("Applications", rows.get(0).getCell(0).getText());
+        Assertions.assertEquals("Primefaces", rows.get(1).getCell(0).getText());
+        Assertions.assertEquals("mobile.app", rows.get(2).getCell(0).getText());
+        Assertions.assertEquals(1, rows.get(0).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+        Assertions.assertEquals(3, rows.get(2).getLevel());
+
+        // Act (filter on L1)
+        treeTable.filter("Name", "down");
+        treeTable.getRow(0).toggle();
+
+        // Assert
+        rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("Downloads", rows.get(0).getCell(0).getText());
+        Assertions.assertEquals("Spanish", rows.get(1).getCell(0).getText());
+        Assertions.assertEquals("Travel", rows.get(2).getCell(0).getText());
+        Assertions.assertEquals(1, rows.get(0).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+
+        // Act
+        page.buttonUpdate.click();
+
+        // Assert - filter must not be lost after update
+        rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("Downloads", rows.get(0).getCell(0).getText());
+        Assertions.assertEquals("Spanish", rows.get(1).getCell(0).getText());
+        Assertions.assertEquals("Travel", rows.get(2).getCell(0).getText());
+        Assertions.assertEquals(1, rows.get(0).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(4)
+    @DisplayName("TreeTable: global filter")
+    public void testGlobalFilter(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act (filter on L3)
+        filterGlobal(page.globalFilter, "mobi");
+
+        // Assert
+        // we try to avoid duplicating logic from TreeTableRenderer#filter, so we take the simple/stupid way
+        List<Row> rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("Applications", rows.get(0).getCell(0).getText());
+        Assertions.assertEquals("Primefaces", rows.get(1).getCell(0).getText());
+        Assertions.assertEquals("mobile.app", rows.get(2).getCell(0).getText());
+        Assertions.assertEquals(1, rows.get(0).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+        Assertions.assertEquals(3, rows.get(2).getLevel());
+
+        // Act (filter on L1)
+        filterGlobal(page.globalFilter, "down");
+        PrimeSelenium.wait(500); //there should be a better way but guardAjax as part of filterGlobal does not work
+        treeTable.getRow(0).toggle();
+
+        // Assert
+        rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("Downloads", rows.get(0).getCell(0).getText());
+        Assertions.assertEquals("Spanish", rows.get(1).getCell(0).getText());
+        Assertions.assertEquals("Travel", rows.get(2).getCell(0).getText());
+        Assertions.assertEquals(1, rows.get(0).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+
+        // Act
+        PrimeSelenium.guardAjax(page.buttonUpdate).click();
+
+        // Assert - filter must not be lost after update
+        rows = treeTable.getRows();
+        Assertions.assertNotNull(rows);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("Downloads", rows.get(0).getCell(0).getText());
+        Assertions.assertEquals("Spanish", rows.get(1).getCell(0).getText());
+        Assertions.assertEquals("Travel", rows.get(2).getCell(0).getText());
+        Assertions.assertEquals(1, rows.get(0).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+        Assertions.assertEquals(2, rows.get(1).getLevel());
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(5)
+    @DisplayName("TreeTable: show selected document (not a TreeTable feature)")
+    public void testShowSelectedDocument(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act
+        PrimeSelenium.guardAjax(treeTable.getRow(0).getCell(3).getWebElement().findElement(By.cssSelector("button"))).click();
+
+        // Assert
+        assertMessage(page.messages, 0, "selected document", "Applications");
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(6)
+    @DisplayName("TreeTable: single select (via TreeTable-selection feature)")
+    public void testShowSelectedNode(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act
+        PrimeSelenium.guardAjax(treeTable.getRow(0).getWebElement()).click();
+        page.buttonShowSelectedNode.click();
+
+        // Assert
+        assertMessage(page.messages, 0, "selected node", "Applications");
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(7)
+    @DisplayName("TreeTable: select-event")
+    public void testSelectedEvent(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act
+        PrimeSelenium.guardAjax(treeTable.getRow(0).getWebElement()).click();
+
+        // Assert
+        assertMessage(page.messages, 0, "select-event", "Applications");
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    @Test
+    @Order(8)
+    @DisplayName("TreeTable: unselect-event")
+    public void testUnselectedEvent(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act
+        PrimeSelenium.guardAjax(treeTable.getRow(0).getWebElement()).click(); //select
+        Actions actions = new Actions(page.getWebDriver());
+        Action actionUnselect = actions.keyDown(Keys.META).click(treeTable.getRow(0).getWebElement()).keyUp(Keys.META).build();
+        PrimeSelenium.guardAjax(actionUnselect).perform();
+
+        // Assert
+        assertMessage(page.messages, 0, "unselect-event", "Applications");
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    private void assertConfiguration(JSONObject cfg) {
+        assertNoJavascriptErrors();
+        System.out.println("TreeTable Config = " + cfg);
+        Assertions.assertEquals("treeTable", cfg.getString("widgetVar"));
+    }
+
+    public static class Page extends AbstractPrimePage {
+        @FindBy(id = "form:treeTable")
+        TreeTable treeTable;
+
+        @FindBy(id = "form:msgs")
+        Messages messages;
+
+        @FindBy(id = "form:treeTable:globalFilter")
+        InputText globalFilter;
+
+        @FindBy(id = "form:buttonUpdate")
+        CommandButton buttonUpdate;
+
+        @FindBy(id = "form:buttonResetTable")
+        CommandButton buttonResetTable;
+
+        @FindBy(id = "form:buttonShowSelectedNode")
+        CommandButton buttonShowSelectedNode;
+
+//        @FindBy(id = "form:buttonGlobalFilterOnly")
+//        CommandButton buttonGlobalFilterOnly;
+
+        @Override
+        public String getLocation() {
+            return "treetable/treeTable001.xhtml";
+        }
+    }
+}
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable002Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable002Test.java
new file mode 100644
index 0000000000..b8ffafb139
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable002Test.java
@@ -0,0 +1,100 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Action;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.support.FindBy;
+import org.primefaces.model.TreeNode;
+import org.primefaces.selenium.AbstractPrimePage;
+import org.primefaces.selenium.PrimeSelenium;
+import org.primefaces.selenium.component.model.TreeTable;
+
+public class TreeTable002Test extends AbstractTreeTableTest {
+
+    @Test
+    @Order(1)
+    @DisplayName("TreeTable: multi sort, predefined sort in markup")
+    public void testMultiSortPlusPredefined(Page page) {
+        // Arrange
+        TreeTable treeTable = page.treeTable;
+        Assertions.assertNotNull(treeTable);
+
+        // ... expand first node
+        treeTable.getRow(0).toggle();
+        TreeNode<Document> treeSorted = sortBy(root, new DocumentSorterNameAscSizeDesc());
+        treeSorted.getChildren().get(0).setExpanded(true);
+
+        // Assert - predefined sort (column 1 asc, column 2 desc)
+        WebElement eltSortName= treeTable.getHeader().getCell(0).getWebElement();
+        WebElement eltSortSize = treeTable.getHeader().getCell(1).getWebElement();
+        assertHeaderSorted(eltSortName, "ASC", 1);
+        assertHeaderSorted(eltSortSize, "DESC", 2);
+        //assertRows(treeTable, treeSorted); // TODO: activate after https://github.com/primefaces/primefaces/issues/7537 is fixed
+
+        // ... undo expand first node
+        treeTable.getRow(0).toggle();
+        treeSorted.getChildren().get(0).setExpanded(false);
+
+        // Act - sort (column 2 desc, column 1 asc)
+        treeTable.sort(1); //asc
+        treeTable.sort(1); //desc
+        Actions actions = new Actions(page.getWebDriver());
+        Action actionUnselect = actions.keyDown(Keys.META).click(treeTable.getHeader().getCell(0).getWebElement()).keyUp(Keys.META).build();
+        PrimeSelenium.guardAjax(actionUnselect).perform();
+        assertHeaderSorted(eltSortName, "ASC", 2);
+        assertHeaderSorted(eltSortSize, "DESC", 1);
+
+        // ... expand first node
+        treeTable.getRow(0).toggle();
+        treeSorted = sortBy(treeSorted, new DocumentSorterSizeDescNameAsc());
+        treeSorted.getChildren().get(0).setExpanded(true);
+        assertRows(treeTable, treeSorted);
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    private void assertConfiguration(JSONObject cfg) {
+        assertNoJavascriptErrors();
+        System.out.println("TreeTable Config = " + cfg);
+        Assertions.assertEquals("wgtTreeTable", cfg.getString("widgetVar"));
+    }
+
+    public static class Page extends AbstractPrimePage {
+        @FindBy(id = "form:treetable")
+        TreeTable treeTable;
+
+        @Override
+        public String getLocation() {
+            return "treetable/treeTable002.xhtml";
+        }
+    }
+}
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable003Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable003Test.java
new file mode 100644
index 0000000000..e516c6fc64
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable003Test.java
@@ -0,0 +1,89 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.support.FindBy;
+import org.primefaces.model.DefaultTreeNode;
+import org.primefaces.selenium.AbstractPrimePage;
+import org.primefaces.selenium.component.CommandButton;
+import org.primefaces.selenium.component.model.TreeTable;
+
+public class TreeTable003Test extends AbstractTreeTableTest {
+
+    @Test
+    @Order(1)
+    @DisplayName("TreeTable: paginator")
+    public void testPaginator(Page page) {
+        // Arrange
+        TreeTable treeTable = page.treeTable;
+        Assertions.assertNotNull(treeTable);
+        treeTable.selectPage(1);
+
+        // Assert
+        int pages = root.getChildren().size() / 3;
+        if (root.getChildren().size() % 3 > 0) {
+            pages++;
+        }
+        Assertions.assertEquals(pages, treeTable.getPaginator().getPages().size());
+
+        DefaultTreeNode<Document> rootPage1 = new DefaultTreeNode<>(root.getData());
+        rootPage1.setChildren(root.getChildren().subList(0, 3));
+        assertRows(treeTable, rootPage1);
+
+        // Act
+        treeTable.selectPage(2);
+
+        // Assert
+        DefaultTreeNode<Document> rootPage2 = new DefaultTreeNode<>(root.getData());
+        rootPage2.setChildren(root.getChildren().subList(0, 3));
+        assertRows(treeTable, rootPage2);
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+    private void assertConfiguration(JSONObject cfg) {
+        assertNoJavascriptErrors();
+        System.out.println("TreeTable Config = " + cfg);
+        Assertions.assertTrue(cfg.has("paginator"));
+        Assertions.assertEquals("wgtTreeTable", cfg.getString("widgetVar"));
+    }
+
+    public static class Page extends AbstractPrimePage {
+        @FindBy(id = "form:treetable")
+        TreeTable treeTable;
+
+        @FindBy(id = "form:buttonUpdate")
+        CommandButton buttonUpdate;
+
+        @Override
+        public String getLocation() {
+            return "treetable/treeTable003.xhtml";
+        }
+    }
+}
diff --git a/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable004Test.java b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable004Test.java
new file mode 100644
index 0000000000..6a65c5fba7
--- /dev/null
+++ b/primefaces-integration-tests/src/test/java/org/primefaces/integrationtests/treetable/TreeTable004Test.java
@@ -0,0 +1,104 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2009-2021 PrimeTek
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package org.primefaces.integrationtests.treetable;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.support.FindBy;
+import org.primefaces.selenium.AbstractPrimePage;
+import org.primefaces.selenium.component.CommandButton;
+import org.primefaces.selenium.component.InputText;
+import org.primefaces.selenium.component.Messages;
+import org.primefaces.selenium.component.model.TreeTable;
+
+public class TreeTable004Test extends AbstractTreeTableTest {
+
+    @Test
+    @Order(6)
+    @DisplayName("TreeTable: multiple select")
+    public void testShowSelectedNodes(Page page) {
+        TreeTable treeTable = page.treeTable;
+        treeTable.sort("Name");
+
+        // Act - select 3 nodes
+        Actions actions = new Actions(page.getWebDriver());
+        actions.keyDown(Keys.META).click(treeTable.getRow(2).getWebElement()).keyUp(Keys.META).perform();
+        treeTable.getRow(0).toggle();
+        actions.keyDown(Keys.META).click(treeTable.getRow(1).getWebElement()).keyUp(Keys.META).perform();
+        actions.keyDown(Keys.META).click(treeTable.getRow(3).getWebElement()).keyUp(Keys.META).perform();
+        page.buttonShowSelectedNodes.click();
+
+        // Assert
+        assertMessage(page.messages, 0, "selected nodes", "Desktop,editor.app,settings.app");
+
+        // Act - unselect 1 node
+        actions.keyDown(Keys.META).click(treeTable.getRow(3).getWebElement()).keyUp(Keys.META).perform();
+        page.buttonShowSelectedNodes.click();
+
+        // Assert
+        assertMessage(page.messages, 0, "selected nodes", "Desktop,editor.app");
+
+        assertConfiguration(treeTable.getWidgetConfiguration());
+    }
+
+
+    private void assertConfiguration(JSONObject cfg) {
+        assertNoJavascriptErrors();
+        System.out.println("TreeTable Config = " + cfg);
+        Assertions.assertEquals("treeTable", cfg.getString("widgetVar"));
+    }
+
+    public static class Page extends AbstractPrimePage {
+        @FindBy(id = "form:treeTable")
+        TreeTable treeTable;
+
+        @FindBy(id = "form:msgs")
+        Messages messages;
+
+        @FindBy(id = "form:treeTable:globalFilter")
+        InputText globalFilter;
+
+        @FindBy(id = "form:buttonUpdate")
+        CommandButton buttonUpdate;
+
+        @FindBy(id = "form:buttonResetTable")
+        CommandButton buttonResetTable;
+
+        @FindBy(id = "form:buttonShowSelectedNodes")
+        CommandButton buttonShowSelectedNodes;
+
+//        @FindBy(id = "form:buttonGlobalFilterOnly")
+//        CommandButton buttonGlobalFilterOnly;
+
+        @Override
+        public String getLocation() {
+            return "treetable/treeTable004.xhtml";
+        }
+    }
+}
diff --git a/primefaces/src/test/java/org/primefaces/model/CheckboxTreeNodeTest.java b/primefaces/src/test/java/org/primefaces/model/CheckboxTreeNodeTest.java
index 593acdd235..03753668d7 100644
--- a/primefaces/src/test/java/org/primefaces/model/CheckboxTreeNodeTest.java
+++ b/primefaces/src/test/java/org/primefaces/model/CheckboxTreeNodeTest.java
@@ -23,17 +23,14 @@
  */
 package org.primefaces.model;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.*;
+
 public class CheckboxTreeNodeTest {
 
-    private TreeNode root;
+    private TreeNode<?> root;
 
     @SuppressWarnings("unused")
 	@BeforeEach
@@ -70,7 +67,7 @@ public class CheckboxTreeNodeTest {
 
     @Test
     public void shouldPropagateDown() {
-        TreeNode work = root.getChildren().get(0).getChildren().get(0);
+        TreeNode<?> work = root.getChildren().get(0).getChildren().get(0);
         assertEquals("Work", work.getData());
 
         work.setSelected(true);
@@ -84,8 +81,8 @@ public class CheckboxTreeNodeTest {
 
     @Test
     public void shouldPropagateUpWhenOneChildOfANodeIsSelected() {
-        TreeNode document = root.getChildren().get(0);
-        TreeNode work = document.getChildren().get(0);
+        TreeNode<?> document = root.getChildren().get(0);
+        TreeNode<?> work = document.getChildren().get(0);
         TreeNode expenses = work.getChildren().get(0);
 
         expenses.setSelected(true);
@@ -98,9 +95,9 @@ public class CheckboxTreeNodeTest {
 
     @Test
     public void shouldPropagateUpWhenAllChildrenOfANodeIsSelected() {
-        TreeNode document = root.getChildren().get(0);
-        TreeNode work = document.getChildren().get(0);
-        TreeNode expenses = work.getChildren().get(0);
+        TreeNode<?> document = root.getChildren().get(0);
+        TreeNode<?> work = document.getChildren().get(0);
+        TreeNode<?> expenses = work.getChildren().get(0);
         TreeNode resume = work.getChildren().get(1);
 
         expenses.setSelected(true);
@@ -153,7 +150,7 @@ public class CheckboxTreeNodeTest {
 
     @Test
     public void shouldUpdateSelectionStateWhenNodesAreRemoved() {
-        TreeNode work = root.getChildren().get(0).getChildren().get(0);
+        TreeNode<?> work = root.getChildren().get(0).getChildren().get(0);
         assertEquals("Work", work.getData());
         assertEquals(2, work.getChildCount());
 
diff --git a/primefaces/src/test/java/org/primefaces/model/TreeNodeTest.java b/primefaces/src/test/java/org/primefaces/model/TreeNodeTest.java
index efdc0a4375..a90b1a64ca 100644
--- a/primefaces/src/test/java/org/primefaces/model/TreeNodeTest.java
+++ b/primefaces/src/test/java/org/primefaces/model/TreeNodeTest.java
@@ -23,11 +23,11 @@
  */
 package org.primefaces.model;
 
+import org.junit.jupiter.api.Test;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 
-import org.junit.jupiter.api.Test;
-
 public class TreeNodeTest {
 
 	@Test
@@ -77,7 +77,7 @@ public class TreeNodeTest {
 	
 	@Test
 	public void shouldHaveConsistenKeys() {
-		TreeNode root = new DefaultTreeNode(null);
+		TreeNode<?> root = new DefaultTreeNode(null);
 
 		TreeNode child0 = new DefaultTreeNode(null, root);
 		TreeNode child1 = new DefaultTreeNode(null, root);