Repository: webforj/webforj
Pull request #937 (2025-03-20 15:26:27)

build(deps): bump org.apache.httpcomponents.core5:httpcore5 from 5.3.3 to 5.3.4
Bumps [org.apache.httpcomponents.core5:httpcore5](https://github.com/apache/httpcomponents-core) from 5.3.3 to 5.3.4.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/apache/httpcomponents-core/blob/rel/v5.3.4/RELEASE_NOTES.txt">org.apache.httpcomponents.core5:httpcore5's changelog</a>.</em></p>
<blockquote>
<h2>Release 5.3.4</h2>
<p>This maintenance release fixes several corner-case defects in the asynchronous HTTP/1.1 protocol
handler as well as several other minor defects.</p>
<h2>Change Log</h2>
<ul>
<li>
<p>HTTPCORE-778: URIBuilder to not encode sub-delimiters in URI fragment component for consistency
with query component encoding
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug fix: Fixed a race condition in the async message stream handlers that can occur when
a message with no content body is being submitted asynchronously.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug fix: Async message stream handlers to avoid firing data produce events from multiple
threads.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>HTTPCORE-777: I/O reactor to cancel out incomplete connect requests when shutting down.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Resource release corrections and code improvements in the base implementations of client
and server async exchange handlers.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug fix: Corrected the state management in the async HTTP/1.1 protocol handler when
committing requests asynchronously.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug-fix: Corrected message stream termination by the async HTTP/1.1 protocol handler
when the connection has been half-closed by the opposite endpoint.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug fix: Shared output buffer #writeCompleted fails to signal the end of the stream correctly
when there is no buffered data.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug fix: Corrected handling of invalid HTTP protocol version by HTTP/1.1 async client
protocol handler.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>Bug fix: Corrected state tracking of pipelined message exchanges by HTTP/1.1 async client
protocol handler.
Contributed by Oleg Kalnichevski <!-- raw HTML omitted --></p>
</li>
<li>
<p>HTTP/1.1 server protocol handler to use exception callback to report exceptions that cannot</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/httpcomponents-core/commit/10e2b0c742e3fe3fefa86421f38ecf4e415bb041"><code>10e2b0c</code></a> HttpCore 5.3.4 release</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/1cd2e82e1cf7afcade33ec999944cb87054eac2d"><code>1cd2e82</code></a> Updated release notes for HttpCore 5.3.4 release</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/b8a45f0954fee694e4ab1c48764274e74f0f84ff"><code>b8a45f0</code></a> HTTPCORE-778: URIBuilder to not encode sub-delims in URI fragment component f...</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/00973e850c2088684ad7a82024c083d5b454bcb2"><code>00973e8</code></a> Bug fix: Fixed a race condition in async message stream handlers that can occ...</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/1bf8e652549befa4fa6b9fe59fb0e7b79cb00798"><code>1bf8e65</code></a> Bug fix: Async message stream handlers to avoid firing data produce events fr...</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/3010ce8ba88403f78f3116abda167950c6073224"><code>3010ce8</code></a> Use atomic refs to keep request and response state in async message stream ha...</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/50749b80730f91c33725e473eb0776a30cb21223"><code>50749b8</code></a> Fixed incorrect state logs in async client HTTP/1.1 protocol handler</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/714155a98dbc667dbd83638f0b30e8a5b9912563"><code>714155a</code></a> HTTPCORE-777: Made I/O reactos cancel out incomplete connect request when shu...</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/5aad6bb138372c2ed5ffd8d9317cff49dd17c2dd"><code>5aad6bb</code></a> Resource release corrections and code improvements in the base implementation...</li>
<li><a href="https://github.com/apache/httpcomponents-core/commit/7274811b278641b46dead575f5ac5c15e052ea09"><code>7274811</code></a> Bug fix: corrected the state management in the async HTTP/1.1 protocol handle...</li>
<li>Additional commits viewable in <a href="https://github.com/apache/httpcomponents-core/compare/rel/v5.3.3...rel/v5.3.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.httpcomponents.core5:httpcore5&package-manager=maven&previous-version=5.3.3&new-version=5.3.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

github-actions[bot]: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
<tr><td><a href="https://github.com/apache/httpcomponents-core"> maven/org.apache.httpcomponents.core5:httpcore5 </a></td><td>5.3.4</td>
      <td>:green_circle: 7.3</td><td><details><summary>Details</summary><table><tr><th>Check</th><th>Score</th><th>Reason</th></tr><tr><td>Code-Review</td><td>:warning: 1</td><td>Found 3/30 approved changesets -- score normalized to 1</td></tr><tr><td>Token-Permissions</td><td>:green_circle: 10</td><td>GitHub workflow tokens follow principle of least privilege</td></tr><tr><td>Dangerous-Workflow</td><td>:green_circle: 10</td><td>no dangerous workflow patterns detected</td></tr><tr><td>CII-Best-Practices</td><td>:warning: 0</td><td>no effort to earn an OpenSSF best practices badge detected</td></tr><tr><td>Packaging</td><td>:warning: -1</td><td>packaging workflow not detected</td></tr><tr><td>Maintained</td><td>:green_circle: 10</td><td>30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10</td></tr><tr><td>Security-Policy</td><td>:green_circle: 10</td><td>security policy file detected</td></tr><tr><td>License</td><td>:green_circle: 10</td><td>license file detected</td></tr><tr><td>Signed-Releases</td><td>:warning: -1</td><td>no releases found</td></tr><tr><td>Branch-Protection</td><td>:warning: 0</td><td>branch protection not enabled on development/release branches</td></tr><tr><td>Binary-Artifacts</td><td>:green_circle: 10</td><td>no binaries found in the repo</td></tr><tr><td>Pinned-Dependencies</td><td>:warning: 0</td><td>dependency not pinned by hash detected -- score normalized to 0</td></tr><tr><td>Fuzzing</td><td>:green_circle: 10</td><td>project is fuzzed</td></tr><tr><td>SAST</td><td>:green_circle: 10</td><td>SAST tool is run on all commits</td></tr><tr><td>Vulnerabilities</td><td>:green_circle: 10</td><td>0 existing vulnerabilities detected</td></tr></table></details></td></tr>
</table><h2>Scanned Files</h2>
<ul><li>webforj-plugins/webforj-install-maven-plugin/pom.xml</li></ul>


<!-- dependency-review-pr-comment-marker -->
sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=webforj&pullRequest=937) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=webforj&pullRequest=937&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=webforj&pullRequest=937&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=webforj&pullRequest=937&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=webforj&pullRequest=937&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=webforj&pullRequest=937&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=webforj&pullRequest=937)


github-actions[bot]: ### Code Coverage
|Overall Project|74.55%|:green_apple:|
|:-|:-|:-:|

> There is no coverage information present for the Files changed

Patch:
diff --git a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Column.java b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Column.java
index fcba52bf..689dd88b 100644
--- a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Column.java
+++ b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Column.java
@@ -127,6 +127,7 @@ public final class Column<T, V> implements Serializable {
   private PinDirection pinned = PinDirection.AUTO;
   private boolean sortable = false;
   private SortDirection sort = SortDirection.NONE;
+  private int sortIndex = 0;
   private boolean suppressNavigable = false;
   private Float minWidth = null;
   @SuppressWarnings("unused")
@@ -377,6 +378,35 @@ public final class Column<T, V> implements Serializable {
     return sort;
   }
 
+  /**
+   * Sets the sort index of the column.
+   *
+   * @param sortIndex the new sort index
+   * @return the column itself
+   *
+   * @throws IllegalArgumentException if the sort index is negative
+   */
+  public Column<T, V> setSortIndex(int sortIndex) {
+    int oldSortIndex = this.sortIndex;
+    if (sortIndex < 0) {
+      throw new IllegalArgumentException("Sort index cannot be negative");
+    }
+
+    this.sortIndex = sortIndex;
+    changeSupport.firePropertyChange("sortIndex", oldSortIndex, sortIndex);
+
+    return this;
+  }
+
+  /**
+   * Returns the sort index of the column.
+   *
+   * @return the sort index
+   */
+  public int getSortIndex() {
+    return sortIndex;
+  }
+
   /**
    * Sets the suppress navigable status of the column.
    *
diff --git a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java
index 9fb03d01..ebf98fec 100644
--- a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java
+++ b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/Table.java
@@ -111,10 +111,6 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
   private Repository<T> repository = new CollectionRepository<>(Collections.emptyList());
   private List<Column<T, ?>> columns = new ArrayList<>();
   private Set<String> selectedKeys = new HashSet<>();
-  private EventListener<TableSortChangeEvent<T>> handleSortChangedListener =
-      this::handleSortChanged;
-  private ListenerRegistration<TableSortChangeEvent<T>> handleSortChangedListenerRegistration =
-      null;
   private boolean registeredValueChangeListener = false;
 
   // Internal properties
@@ -145,6 +141,8 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
   private final PropertyDescriptor<Double> overscan = PropertyDescriptor.property("overscan", 35d);
   private final PropertyDescriptor<Boolean> clientSorting =
       PropertyDescriptor.property("clientSorting", false);
+  private final PropertyDescriptor<Boolean> multiSorting =
+      PropertyDescriptor.property("multiSorting", true);
 
   /**
    * Construct a new Table.
@@ -655,22 +653,36 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
    * @return the component itself
    */
   public Table<T> setClientSorting(boolean enabled) {
-    // enable or disable server sorting listener
-    if (enabled) {
-      if (handleSortChangedListenerRegistration != null) {
-        handleSortChangedListenerRegistration.remove();
-        handleSortChangedListenerRegistration = null;
-      }
-    } else {
-      if (handleSortChangedListenerRegistration == null) {
-        handleSortChangedListenerRegistration = addSortChangeListener(handleSortChangedListener);
-      }
-    }
-
     set(clientSorting, enabled);
     return this;
   }
 
+  /**
+   * Enables or disables multi-column sorting.
+   *
+   * <p>
+   * By default, only one column can be sorted at a time. Clicking on a column header will sort the
+   * column and remove the sorting from the other columns. Setting this property to {@code true}
+   * allows multiple columns to be sorted at once.
+   * </p>
+   *
+   * @param value {@code false} to disable multi-column sorting, {@code true} to enable
+   * @return the component itself
+   */
+  public Table<T> setMultiSorting(boolean value) {
+    set(multiSorting, value);
+    return this;
+  }
+
+  /**
+   * Checks if multiple columns can be sorted at once.
+   *
+   * @return {@code false} if only one column can be sorted at a time, {@code true} otherwise
+   */
+  public boolean isMultiSorting() {
+    return get(multiSorting);
+  }
+
   /**
    * Checks if client-side sorting is enabled.
    *
@@ -1009,10 +1021,7 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
   void onInit(Element el) {
     this.refresh();
     set(selectedProp, selectedKeys);
-
-    if (!isClientSorting()) {
-      handleSortChangedListenerRegistration = addSortChangeListener(handleSortChangedListener);
-    }
+    addSortChangeListener(this::handleSortChanged);
   }
 
   Void onInitFailed(Throwable e) {
@@ -1028,8 +1037,12 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
    */
   OrderCriteriaList<T> getOrderCriteriaList() {
     OrderCriteriaList<T> criterion = new OrderCriteriaList<>();
+    // create a copy of columns sorted based on the sortIndex
+    List<Column<T, ?>> sortedColumns = new ArrayList<>(columns);
+    sortedColumns.sort(Comparator.comparingInt(Column::getSortIndex));
 
-    for (Column<T, ?> column : columns) {
+    // iterate over the sorted columns and add the order criteria
+    for (Column<T, ?> column : sortedColumns) {
       if (column.getSortDirection().equals(Column.SortDirection.NONE)) {
         continue;
       }
@@ -1104,8 +1117,8 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
 
   void handleSortChanged(TableSortChangeEvent<T> e) {
     Map<String, String> clientCriterion = e.getClientCriterion();
+    List<String> sortedColumnIds = new ArrayList<>(clientCriterion.keySet());
 
-    // update the columns with the new sort directions
     for (Column<T, ?> column : getColumns()) {
       String id = column.getId();
       String direction = clientCriterion.get(id);
@@ -1113,12 +1126,22 @@ public final class Table<T> extends HtmlComponent<Table<T>> implements HasReposi
       if (direction != null) {
         column.setSortDirection("asc".equalsIgnoreCase(direction) ? Column.SortDirection.ASC
             : Column.SortDirection.DESC);
+
+        int index = sortedColumnIds.indexOf(id);
+        if (index == -1) {
+          throw new IllegalStateException("The column " + id + " is not in the sorted columns");
+        }
+
+        column.setSortIndex(index + 1);
       } else {
         column.setSortDirection(Column.SortDirection.NONE);
+        column.setSortIndex(0);
       }
     }
 
-    getRepository().commit();
+    if (!isClientSorting()) {
+      getRepository().commit();
+    }
   }
 
   String[] mapKeys(Object... keys) {
diff --git a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/event/TableSortChangeEvent.java b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/event/TableSortChangeEvent.java
index 11409e3e..53c4ceaa 100644
--- a/webforj-components/webforj-table/src/main/java/com/webforj/component/table/event/TableSortChangeEvent.java
+++ b/webforj-components/webforj-table/src/main/java/com/webforj/component/table/event/TableSortChangeEvent.java
@@ -1,5 +1,7 @@
 package com.webforj.component.table.event;
 
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
 import com.webforj.component.element.annotation.EventName;
 import com.webforj.component.element.annotation.EventOptions;
 import com.webforj.component.element.annotation.EventOptions.EventData;
@@ -7,8 +9,10 @@ import com.webforj.component.table.Column;
 import com.webforj.component.table.Table;
 import com.webforj.data.repository.OrderCriteria;
 import com.webforj.data.repository.OrderCriteriaList;
+import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 
@@ -21,9 +25,10 @@ import java.util.function.Function;
  * @since 24.00
  */
 @EventName("dwc-sort-changed")
-@EventOptions(data = {@EventData(key = "criteria", exp = "event.detail")})
+@EventOptions(data = {@EventData(key = "criteria", exp = "JSON.stringify(event.detail)")})
 public class TableSortChangeEvent<T> extends TableEvent<T> {
-  private Map<String, String> clientCriteria = new HashMap<>();
+  private final transient Map<String, String> clientCriteria = new LinkedHashMap<>();
+  private final transient List<ClientCriteria> criteriaDetails;
 
   /**
    * Creates a new sort event.
@@ -33,16 +38,32 @@ public class TableSortChangeEvent<T> extends TableEvent<T> {
    */
   public TableSortChangeEvent(Table<T> table, Map<String, Object> eventMap) {
     super(table, eventMap);
-    this.clientCriteria = (Map<String, String>) eventMap.get("criteria");
+    TypeToken<List<ClientCriteria>> criteriaListToken = new TypeToken<List<ClientCriteria>>() {};
+    this.criteriaDetails =
+        new Gson().fromJson((String) eventMap.get("criteria"), criteriaListToken.getType());
   }
 
   /**
-   * Returns the client side sort criteria.
+   * Returns the client side sort criteria as a map.
+   *
+   * <p>
+   * This method is used to get the client side sort criteria. The criteria are stored in a map
+   * where the key is the column id and the value is the sort direction (asc or desc).
+   * </p>
    *
    * @return the client side sort criteria
    */
   public Map<String, String> getClientCriterion() {
-    return clientCriteria;
+    // no need to sort the criteria, it is already sorted by the client
+    if (clientCriteria.isEmpty()) {
+      for (ClientCriteria clientCriterion : criteriaDetails) {
+        String columnId = clientCriterion.getId();
+        String sort = clientCriterion.getSort();
+        clientCriteria.put(columnId, sort);
+      }
+    }
+
+    return Collections.unmodifiableMap(clientCriteria);
   }
 
   /**
@@ -54,7 +75,8 @@ public class TableSortChangeEvent<T> extends TableEvent<T> {
     OrderCriteriaList<T> criterion = new OrderCriteriaList<>();
 
     // loop over the criteria and create a list of OrderCriteria
-    for (Map.Entry<String, String> entry : clientCriteria.entrySet()) {
+    var localClientCriterion = getClientCriterion();
+    for (var entry : localClientCriterion.entrySet()) {
       String columnId = entry.getKey();
       OrderCriteria.Direction direction =
           entry.getValue().equalsIgnoreCase("asc") ? OrderCriteria.Direction.ASC
@@ -72,4 +94,52 @@ public class TableSortChangeEvent<T> extends TableEvent<T> {
 
     return criterion;
   }
+
+  /**
+   * Represents the client criteria.
+   *
+   * @return the OrderCriteriaList
+   * @since 25.00
+   */
+  static final class ClientCriteria {
+    private String id;
+    private String sort;
+    private int sortIndex;
+
+    /**
+     * Get the id of the column.
+     *
+     * @return the id of the column
+     */
+    public String getId() {
+      return id;
+    }
+
+    /**
+     * Get the sort direction.
+     *
+     * @return the sort direction
+     */
+    public String getSort() {
+      return sort;
+    }
+
+    /**
+     * Get the sort index.
+     *
+     * @return the sort index
+     */
+    public int getSortIndex() {
+      return sortIndex;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+      return "ClientCriteria{" + "id='" + id + '\'' + ", sort='" + sort + '\'' + ", sortIndex="
+          + sortIndex + '}';
+    }
+  }
 }
diff --git a/webforj-data/src/main/java/com/webforj/data/repository/CompositeComparator.java b/webforj-data/src/main/java/com/webforj/data/repository/CompositeComparator.java
index 9a924b60..64dde9f6 100644
--- a/webforj-data/src/main/java/com/webforj/data/repository/CompositeComparator.java
+++ b/webforj-data/src/main/java/com/webforj/data/repository/CompositeComparator.java
@@ -68,14 +68,10 @@ public class CompositeComparator<T> implements Comparator<T> {
         return 0;
       }
 
-      if (o1 == null) {
+      if (o1 == null || o2 == null) {
         return direction == OrderCriteria.Direction.ASC ? -1 : 1;
       }
 
-      if (o2 == null) {
-        return direction == OrderCriteria.Direction.ASC ? 1 : -1;
-      }
-
       try {
         if (criterion.getComparator() != null) {
           // Use the custom comparator if provided
@@ -120,6 +116,11 @@ public class CompositeComparator<T> implements Comparator<T> {
    */
   protected <V extends Comparable<V>> int compareWithDirection(V value1, V value2,
       OrderCriteria.Direction direction) {
+
+    if (value1 == null || value2 == null) {
+      return direction == OrderCriteria.Direction.ASC ? -1 : 1;
+    }
+
     int comparison = value1.compareTo(value2);
     return direction == OrderCriteria.Direction.ASC ? comparison : -comparison;
   }
diff --git a/webforj-plugins/webforj-install-maven-plugin/pom.xml b/webforj-plugins/webforj-install-maven-plugin/pom.xml
index 73255d9b..6d30dea4 100644
--- a/webforj-plugins/webforj-install-maven-plugin/pom.xml
+++ b/webforj-plugins/webforj-install-maven-plugin/pom.xml
@@ -13,7 +13,7 @@
   <packaging>maven-plugin</packaging>
 
   <properties>
-    <httpcore5.version>5.3.3</httpcore5.version>
+    <httpcore5.version>5.3.4</httpcore5.version>
     <httpclient5.version>5.4.2</httpclient5.version>
     <arguments/>
   </properties>


Test patch:
diff --git a/webforj-components/webforj-table/src/test/java/com/webforj/component/table/TableTest.java b/webforj-components/webforj-table/src/test/java/com/webforj/component/table/TableTest.java
index b2dffc68..fd525e09 100644
--- a/webforj-components/webforj-table/src/test/java/com/webforj/component/table/TableTest.java
+++ b/webforj-components/webforj-table/src/test/java/com/webforj/component/table/TableTest.java
@@ -396,20 +396,33 @@ class TableTest {
       component.addColumn("col3", Function.identity()).setSortDirection(Column.SortDirection.DESC);
 
       Map<String, Object> payload = new HashMap<>();
-      payload.put("criteria", new HashMap<>() {
-        {
-          put("col1", "asc");
-          put("col2", "desc");
-        }
-      });
+      payload.put("criteria", """
+          [
+            {
+              "id": "col1",
+              "sort": "asc",
+              "sortIndex": 1
+            },
+            {
+              "id": "col2",
+              "sort": "asc",
+              "sortIndex": 2
+            },
+            {
+              "id": "col3",
+              "sort": "desc",
+              "sortIndex": 3
+            }
+          ]
+          """);
       TableSortChangeEvent<String> event = new TableSortChangeEvent<>(component, payload);
 
       component.handleSortChanged(event);
 
       // check columns directions are updated
       assertEquals(Column.SortDirection.ASC, component.getColumnById("col1").getSortDirection());
-      assertEquals(Column.SortDirection.DESC, component.getColumnById("col2").getSortDirection());
-      assertEquals(Column.SortDirection.NONE, component.getColumnById("col3").getSortDirection());
+      assertEquals(Column.SortDirection.ASC, component.getColumnById("col2").getSortDirection());
+      assertEquals(Column.SortDirection.DESC, component.getColumnById("col3").getSortDirection());
     }
 
     @Test
diff --git a/webforj-components/webforj-table/src/test/java/com/webforj/component/table/event/TableSortChangeEventTest.java b/webforj-components/webforj-table/src/test/java/com/webforj/component/table/event/TableSortChangeEventTest.java
index 69f9b0c5..01794a7c 100644
--- a/webforj-components/webforj-table/src/test/java/com/webforj/component/table/event/TableSortChangeEventTest.java
+++ b/webforj-components/webforj-table/src/test/java/com/webforj/component/table/event/TableSortChangeEventTest.java
@@ -13,26 +13,56 @@ import org.junit.jupiter.api.Test;
 
 class TableSortChangeEventTest {
 
-  Table<String> table = null;
+  Table<Person> table = null;
 
   @BeforeEach
   void setup() {
-    table = new Table<String>();
-    table.addColumn("item", String::valueOf).setLabel("Items");
-    table.setItems(List.of("Item 0"));
+    table = new Table<Person>();
+    table.addColumn("name", Person::name);
+    table.addColumn("city", Person::city);
+    table.addColumn("age", Person::age);
+
+    table.setItems(List.of(new Person("Hyyan", "Syria", 30), new Person("Stephen", "DE", 25),
+        new Person("John", "US", 35), new Person("Jane", "UK", 28)));
   }
 
   @Test
   void shouldReturnSortCriteria() {
     Map<String, Object> eventMap = new HashMap<>();
-    eventMap.put("criteria", Map.of("item", "desc"));
+    eventMap.put("criteria", """
+        [
+          {
+            "id": "name",
+            "sort": "asc",
+            "sortIndex": 1
+          },
+          {
+            "id": "city",
+            "sort": "asc",
+            "sortIndex": 2
+          },
+          {
+            "id": "age",
+            "sort": "desc",
+            "sortIndex": 3
+          }
+        ]
+        """);
+
+    TableSortChangeEvent<Person> event = new TableSortChangeEvent<>(table, eventMap);
 
-    TableSortChangeEvent<String> event = new TableSortChangeEvent<>(table, eventMap);
+    OrderCriteriaList<Person> criterion = event.getOrderCriteriaList();
+    assertEquals(3, criterion.size());
 
-    OrderCriteriaList<String> criterion = event.getOrderCriteriaList();
-    assertEquals(1, criterion.size());
+    OrderCriteria<Person, ?> criteria = criterion.iterator().next();
+    assertEquals(OrderCriteria.Direction.ASC, criteria.getDirection());
+  }
 
-    OrderCriteria<String, ?> criteria = criterion.iterator().next();
-    assertEquals(OrderCriteria.Direction.DESC, criteria.getDirection());
+  static record Person(String name, String city, int age) {
+    public Person(String name, String city, int age) {
+      this.name = name;
+      this.city = city;
+      this.age = age;
+    }
   }
 }
diff --git a/webforj-data/src/test/java/com/webforj/data/repository/CompositeComparatorTest.java b/webforj-data/src/test/java/com/webforj/data/repository/CompositeComparatorTest.java
index c9a3b21b..38c12c32 100644
--- a/webforj-data/src/test/java/com/webforj/data/repository/CompositeComparatorTest.java
+++ b/webforj-data/src/test/java/com/webforj/data/repository/CompositeComparatorTest.java
@@ -148,7 +148,7 @@ class CompositeComparatorTest {
         new CompositeComparator<>(new OrderCriteriaList<>(lengthCriteria));
 
     assertTrue(comparator.compare(null, "abc") < 0);
-    assertTrue(comparator.compare("abc", null) > 0);
+    assertTrue(comparator.compare("abc", null) < 0);
     assertEquals(0, comparator.compare(null, null));
   }
 
@@ -168,7 +168,7 @@ class CompositeComparatorTest {
     Person person2 = new Person("Charlotte");
 
     assertTrue(comparator.compare(null, person1) < 0);
-    assertTrue(comparator.compare(person2, null) > 0);
+    assertTrue(comparator.compare(person2, null) < 0);
     assertEquals(0, comparator.compare(person1, person1));
   }