diff --git a/.github/workflows/snapshot_release.yml b/.github/workflows/snapshot_release.yml
index 97fdad8e8..46b8b0fd9 100644
--- a/.github/workflows/snapshot_release.yml
+++ b/.github/workflows/snapshot_release.yml
@@ -42,11 +42,6 @@ jobs:
           ref: ${{ github.event.pull_request.head.ref }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}

-      - name: Set up secrets
-        run: |
-          echo "CYPRESS_RECORD_KEY=$(echo YzE3ZDU4OGItZTBkOC00ZjJmLTg4NjYtNzJmNmFmYmRhNGQxCg== | base64 -d)" >> $GITHUB_ENV
-          echo "CYPRESS_PROJECT_ID=s5du3k" >> $GITHUB_ENV
-
       - name: Set up Java 11
         uses: actions/setup-java@v4
         with:
@@ -87,8 +82,8 @@ jobs:
           ./refine e2e_tests
         env:
           CYPRESS_BROWSER: ${{ matrix.browser }}
-          CYPRESS_RECORD_KEY: ${{ env.CYPRESS_RECORD_KEY }}
-          CYPRESS_PROJECT_ID: ${{ env.CYPRESS_PROJECT_ID }}
+          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+          CYPRESS_PROJECT_ID: ${{ vars.CYPRESS_PROJECT_ID }}
           CYPRESS_CI_BUILD_ID: '${{ github.run_id }}'
           CYPRESS_SPECS: ${{ matrix.specs.paths }}
           CYPRESS_GROUP: ${{ matrix.specs.group }}
diff --git a/benchmark/pom.xml b/benchmark/pom.xml
index 720be7997..c3c649056 100644
--- a/benchmark/pom.xml
+++ b/benchmark/pom.xml
@@ -141,7 +141,7 @@
         </plugin>
         <plugin>
           <artifactId>maven-site-plugin</artifactId>
-          <version>3.20.0</version>
+          <version>3.21.0</version>
         </plugin>
         <plugin>
           <artifactId>maven-source-plugin</artifactId>
diff --git a/extensions/database/pom.xml b/extensions/database/pom.xml
index 223271ac5..55b2d2507 100644
--- a/extensions/database/pom.xml
+++ b/extensions/database/pom.xml
@@ -109,12 +109,12 @@
     <dependency>
       <groupId>org.mariadb.jdbc</groupId>
       <artifactId>mariadb-java-client</artifactId>
-      <version>3.4.1</version>
+      <version>3.5.0</version>
     </dependency>
     <dependency>
       <groupId>org.xerial</groupId>
       <artifactId>sqlite-jdbc</artifactId>
-      <version>3.46.1.3</version>
+      <version>3.47.0.0</version>
     </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
diff --git a/modules/core/src/main/java/com/google/refine/importing/ImportingUtilities.java b/modules/core/src/main/java/com/google/refine/importing/ImportingUtilities.java
index 0e4898964..7942f67e3 100644
--- a/modules/core/src/main/java/com/google/refine/importing/ImportingUtilities.java
+++ b/modules/core/src/main/java/com/google/refine/importing/ImportingUtilities.java
@@ -301,7 +301,6 @@ public class ImportingUtilities {
                     JSONUtilities.safePut(fileRecord, "origin", "clipboard");
                     JSONUtilities.safePut(fileRecord, "declaredEncoding", encoding);
                     JSONUtilities.safePut(fileRecord, "declaredMimeType", (String) null);
-                    JSONUtilities.safePut(fileRecord, "format", "text");
                     JSONUtilities.safePut(fileRecord, "fileName", "(clipboard)");
                     JSONUtilities.safePut(fileRecord, "location", getRelativePath(file, rawDataDir));

@@ -309,6 +308,7 @@ public class ImportingUtilities {
                             calculateProgressPercent(update.totalExpectedSize, update.totalRetrievedSize));

                     JSONUtilities.safePut(fileRecord, "size", saveStreamToFile(stream, file, null));
+                    JSONUtilities.safePut(fileRecord, "format", guessBetterFormat(file, fileRecord));
                     JSONUtilities.append(fileRecords, fileRecord);

                     clipboardCount++;
@@ -439,7 +439,6 @@ public class ImportingUtilities {
                             calculateProgressPercent(update.totalExpectedSize, update.totalRetrievedSize));

                     JSONUtilities.safePut(fileRecord, "size", saveStreamToFile(stream, file, null));
-                    JSONUtilities.safePut(fileRecord, "format", guessBetterFormat(file, request.getCharacterEncoding(), "text"));
                     // TODO: This needs to be refactored to be able to test import from archives
                     if (postProcessRetrievedFile(rawDataDir, file, fileRecord, fileRecords, progress)) {
                         archiveCount++;
@@ -700,10 +699,7 @@ public class ImportingUtilities {

     static public void postProcessSingleRetrievedFile(File file, ObjectNode fileRecord) {
         if (!fileRecord.has("format")) {
-            JSONUtilities.safePut(fileRecord, "format",
-                    ImportingManager.getFormat(
-                            file.getName(),
-                            JSONUtilities.getString(fileRecord, "declaredMimeType", null)));
+            JSONUtilities.safePut(fileRecord, "format", guessBetterFormat(file, fileRecord));
         }
     }

@@ -1048,6 +1044,13 @@ public class ImportingUtilities {
         return bestFormat;
     }

+    static String guessBetterFormat(File file, ObjectNode fileRecord) {
+        String encoding = JSONUtilities.getString(fileRecord, "declaredEncoding", null);
+        String bestFormat = ImportingManager.getFormat(file.getName(), JSONUtilities.getString(fileRecord, "declaredMimeType", null));
+        bestFormat = bestFormat == null ? "text" : bestFormat;
+        return guessBetterFormat(file, encoding, bestFormat);
+    }
+
     static void rankFormats(ImportingJob job, final String bestFormat, ArrayNode rankedFormats) {
         final Map<String, String[]> formatToSegments = new HashMap<String, String[]>();

diff --git a/pom.xml b/pom.xml
index 53724fe57..8c4f6e554 100644
--- a/pom.xml
+++ b/pom.xml
@@ -101,8 +101,8 @@
     <odfdom-java.version>0.12.0</odfdom-java.version> <!-- do not update to 0.10.0, see issue #4397 -->
     <vicino.version>1.2.1</vicino.version>
     <clojure.version>1.12.0</clojure.version>
-    <httpclient5.version>5.4</httpclient5.version>
-    <httpcore5.version>5.3</httpcore5.version>
+    <httpclient5.version>5.4.1</httpclient5.version>
+    <httpcore5.version>5.3.1</httpcore5.version>
     <univocity-csv.version>2.9.1</univocity-csv.version>
     <swc-parser-lazy.version>3.1.9</swc-parser-lazy.version>
     <commons-io.version>2.17.0</commons-io.version>
@@ -119,8 +119,8 @@
     <build-helper-maven-plugin.version>3.6.0</build-helper-maven-plugin.version>
     <maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
     <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
-    <exec-maven-plugin.version>3.4.1</exec-maven-plugin.version>
-    <maven-dependency-plugin.version>3.8.0</maven-dependency-plugin.version>
+    <exec-maven-plugin.version>3.5.0</exec-maven-plugin.version>
+    <maven-dependency-plugin.version>3.8.1</maven-dependency-plugin.version>
     <jacoco-maven-plugin.version>0.8.12</jacoco-maven-plugin.version>
     <surefire.version>3.5.1</surefire.version>
     <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>

diff --git a/.github/workflows/pull_request_e2e.yml b/.github/workflows/pull_request_e2e.yml
index fb09561a5..0b0810892 100644
--- a/.github/workflows/pull_request_e2e.yml
+++ b/.github/workflows/pull_request_e2e.yml
@@ -40,11 +40,6 @@ jobs:
     steps:
       - uses: actions/checkout@v4

-      - name: Set up secrets
-        run: |
-          echo "CYPRESS_RECORD_KEY=$(echo YzE3ZDU4OGItZTBkOC00ZjJmLTg4NjYtNzJmNmFmYmRhNGQxCg== | base64 -d)" >> $GITHUB_ENV
-          echo "CYPRESS_PROJECT_ID=s5du3k" >> $GITHUB_ENV
-
       - name: Set up Java 17
         uses: actions/setup-java@v4
         with:
@@ -85,8 +80,8 @@ jobs:
           ./refine e2e_tests
         env:
           CYPRESS_BROWSER: ${{ matrix.browser }}
-          CYPRESS_RECORD_KEY: ${{ env.CYPRESS_RECORD_KEY }}
-          CYPRESS_PROJECT_ID: ${{ env.CYPRESS_PROJECT_ID }}
+          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+          CYPRESS_PROJECT_ID: ${{ vars.CYPRESS_PROJECT_ID }}
           CYPRESS_CI_BUILD_ID: '${{ github.run_id }}'
           CYPRESS_SPECS: ${{ matrix.specs.paths }}
           CYPRESS_GROUP: ${{ matrix.specs.group }}
diff --git a/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.cy.js b/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.cy.js
index 8d94a1985..3b191f161 100644
--- a/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.cy.js
+++ b/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.cy.js
@@ -4,25 +4,23 @@
  */

 /*
-   IMPORTANT: Any test reliant on Facet Actions (Edit,Include/Exclude) should wait 100 msec to ensure the event
+   IMPORTANT: Any test reliant on Facet Actions (Edit,Include/Exclude) should wait >100 msec to ensure the event
    handlers are setup, otherwise clicking on the action will do nothing.
    Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
  */
-const clickFacetAction = (facetType, text, action, setVisibility = false) => {
+const clickFacetAction = (facetType, text, action) => {
   cy.getFacetContainer(facetType)
-      .contains(text)
-      .parent()
-      .within(() => {
-        const elem = cy.contains(action);
-
-        if (setVisibility) {
-          elem.invoke('css', 'visibility', 'visible')
-              .should('have.css', 'visibility', 'visible')
-              .click();
-        } else {
-          elem.click();
-        }
-      });
+    .contains(text)
+    .parent()
+    .invoke('trigger', 'mouseenter'); // include/exclude not visible until we mouse over
+  // start a new chain after the trigger
+  cy.getFacetContainer(facetType)
+    .contains(text)
+    .siblings()
+    .get(".facet-choice-toggle")
+    .contains(action)
+    .should('have.css', 'visibility', 'visible') // partially occluded, so "be.visible" won't work
+    .click();
 };

 describe(__filename, function () {
@@ -226,28 +224,28 @@ describe(__filename, function () {

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
-    clickFacetAction('Shrt_Desc','ALLSPICE,GROUND', 'include', true);
+    cy.wait(150);
+    clickFacetAction('Shrt_Desc','ALLSPICE,GROUND', 'include');
     cy.getCell(0, 'Shrt_Desc').should('contain', 'ALLSPICE,GROUND');
     cy.get('#tool-panel').contains('1 matching rows');

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
-    clickFacetAction('Shrt_Desc','ANISE SEED', 'include', true);
+    cy.wait(150);
+    clickFacetAction('Shrt_Desc','ANISE SEED', 'include');
     cy.getCell(1, 'Shrt_Desc').should('contain', 'ANISE SEED');
     cy.get('#tool-panel').contains('2 matching rows');

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
-    clickFacetAction('Shrt_Desc','BUTTER OIL,ANHYDROUS', 'include', true);
+    cy.wait(150);
+    clickFacetAction('Shrt_Desc','BUTTER OIL,ANHYDROUS', 'include');
     cy.getCell(0, 'Shrt_Desc').should('contain', 'BUTTER OIL,ANHYDROUS');
     cy.get('#tool-panel').contains('3 matching rows');

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
+    cy.wait(150);
     clickFacetAction('Shrt_Desc','ALLSPICE,GROUND', 'exclude');
     cy.get('#tool-panel').contains('2 matching rows');
   });
@@ -258,9 +256,9 @@ describe(__filename, function () {

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
+    cy.wait(150);
     // do a basic facetting, expect 1 row
-    clickFacetAction('Shrt_Desc','ALLSPICE,GROUND','include',true);
+    clickFacetAction('Shrt_Desc','ALLSPICE,GROUND','include');
     cy.getCell(0, 'Shrt_Desc').should('to.contain', 'ALLSPICE,GROUND');
     cy.get('#tool-panel').contains('1 matching rows');

@@ -285,7 +283,7 @@ describe(__filename, function () {

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
+    cy.wait(150);
     // do a basic facetting, expect 1 row
     clickFacetAction('Shrt_Desc','ALLSPICE,GROUND','include',true);
     cy.get('#tool-panel').contains('1 matching rows');
@@ -342,7 +340,7 @@ describe(__filename, function () {

     // Wait to ensure action click handler is setup
     // Ref:  window.setTimeout(wireEvents, 100); in list-facet.js
-    cy.wait(100);
+    cy.wait(150);

     cy.get('div.facet-body-inner > div:nth-child(8)')
         .contains('15.87')
@@ -380,27 +378,15 @@ describe(__filename, function () {
     );
   });

-  // // This test is unstable, mouseover behavior is unpredictable
-  // // This might be because the element is detached from the DOM in the middle
-  // // it('Test include/exlude toggle', function () {
-  // // 	cy.loadAndVisitProject('food.small');
-  // // 	cy.columnActionClick('NDB_No', ['Facet', 'Text facet']);
-  // // 	cy.getFacetContainer('NDB_No').find('.facet-choice[choiceindex="0"]').trigger('mouseover');
-  // // 	cy.getFacetContainer('NDB_No').find('.facet-choice[choiceindex="0"] a.facet-choice-toggle').contains('include').should('be.visible');
-  // // 	cy.getFacetContainer('NDB_No').find('.facet-choice[choiceindex="0"] a.facet-choice-toggle').click();
-  // // 	cy.getFacetContainer('NDB_No').find('.facet-choice[choiceindex="0"] a.facet-choice-toggle').contains('exclude');
-  // // 	cy.getFacetContainer('NDB_No').find('.facet-choice[choiceindex="0"] a.facet-choice-toggle').click();
-  // // 	cy.getFacetContainer('NDB_No').find('.facet-choice[choiceindex="0"] a.facet-choice-toggle').contains('include');
-  // // });
-
-  // it('Test collapsing facet panels', function () {
-  // The following test does not work
-  // Because the facet panels uses soem weird CSS with overflow:hidden, Cypress can not detect it
-  // //// # cy.loadAndVisitProject('food.small');
-  // //// # cy.columnActionClick('NDB_No', ['Facet', 'Text facet']);
-  // //// # ensure facet inner panel is visible
-  // //// # cy.get('#refine-tabs-facets .facets-container li:nth-child(1) .facet-body-inner').should('be.visible');
-  // //// #collapse the panel
-  // //// # cy.get('#refine-tabs-facets .facets-container li:nth-child(1) a[bind="minimizeButton"]').click();
-  // ////
+  it('Test collapsing facet panels', function () {
+    cy.loadAndVisitProject('food.small');
+    cy.columnActionClick('NDB_No', ['Facet', 'Text facet']);
+    // ensure facet inner panel is visible
+    cy.get('#refine-tabs-facets .facets-container li:nth-child(1) .facet-body-inner').should('be.visible');
+    // collapse the panel
+    cy.get('#refine-tabs-facets .facets-container li:nth-child(1) a[bind="minimizeButton"]').click();
+    // Make sure the body is hidden
+    cy.get('#refine-tabs-facets .facets-container li:nth-child(1) .facet-body').should('not.be.visible');
+  });
+
 });
diff --git a/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/rows_records.cy.js b/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/rows_records.cy.js
index d1555205c..37ad9315a 100644
--- a/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/rows_records.cy.js
+++ b/main/tests/cypress/cypress/e2e/project/grid/viewpanel-header/rows_records.cy.js
@@ -1,137 +1,7 @@
-const jsonValue = `[
-  {
-     "id":"0001",
-     "type":"donut",
-     "name":"Cake",
-     "ppu":0.55,
-     "batters":{
-        "batter":[
-           {
-              "id":"1001",
-              "type":"Regular"
-           },
-           {
-              "id":"1002",
-              "type":"Chocolate"
-           },
-           {
-              "id":"1003",
-              "type":"Blueberry"
-           },
-           {
-              "id":"1004",
-              "type":"Devil's Food"
-           }
-        ]
-     },
-     "topping":[
-        {
-           "id":"5001",
-           "type":"None"
-        },
-        {
-           "id":"5002",
-           "type":"Glazed"
-        },
-        {
-           "id":"5005",
-           "type":"Sugar"
-        },
-        {
-           "id":"5007",
-           "type":"Powdered Sugar"
-        },
-        {
-           "id":"5006",
-           "type":"Chocolate with Sprinkles"
-        },
-        {
-           "id":"5003",
-           "type":"Chocolate"
-        },
-        {
-           "id":"5004",
-           "type":"Maple"
-        }
-     ]
-  },
-  {
-     "id":"0002",
-     "type":"donut",
-     "name":"Raised",
-     "ppu":0.55,
-     "batters":{
-        "batter":[
-           {
-              "id":"1001",
-              "type":"Regular"
-           }
-        ]
-     },
-     "topping":[
-        {
-           "id":"5001",
-           "type":"None"
-        },
-        {
-           "id":"5002",
-           "type":"Glazed"
-        },
-        {
-           "id":"5005",
-           "type":"Sugar"
-        },
-        {
-           "id":"5003",
-           "type":"Chocolate"
-        },
-        {
-           "id":"5004",
-           "type":"Maple"
-        }
-     ]
-  },
-  {
-     "id":"0003",
-     "type":"donut",
-     "name":"Old Fashioned",
-     "ppu":0.55,
-     "batters":{
-        "batter":[
-           {
-              "id":"1001",
-              "type":"Regular"
-           },
-           {
-              "id":"1002",
-              "type":"Chocolate"
-           }
-        ]
-     },
-     "topping":[
-        {
-           "id":"5001",
-           "type":"None"
-        },
-        {
-           "id":"5002",
-           "type":"Glazed"
-        },
-        {
-           "id":"5003",
-           "type":"Chocolate"
-        },
-        {
-           "id":"5004",
-           "type":"Maple"
-        }
-     ]
-  }
-]`;
 describe(__filename, function () {
   afterEach(() => {
-    cy.addProjectForDeletion();
-  });
+    cy.addProjectForDeletion()
+  })

   it('ensures rows and records display same in csv file', function () {
     cy.loadAndVisitProject('food.small');
@@ -146,7 +16,9 @@ describe(__filename, function () {
   });
   it('ensures rows and records are different for 3-level json file', function () {
     const projectName = Date.now();
-    cy.loadAndVisitSampleJSONProject(projectName, jsonValue);
+    cy.fixture('donut-records').then((jsonValue) => {
+      cy.loadAndVisitSampleJSONProject(projectName, jsonValue);
+    });
     cy.get('span[bind="modeSelectors"]').contains('records').click();
     cy.get('span:contains("records")').should('length',3);
     cy.get('tr td:nth-child(3)').then((recordNumber) => {
diff --git a/main/tests/cypress/cypress/fixtures/donut-records.json b/main/tests/cypress/cypress/fixtures/donut-records.json
new file mode 100644
index 000000000..3d6fac6b1
--- /dev/null
+++ b/main/tests/cypress/cypress/fixtures/donut-records.json
@@ -0,0 +1,130 @@
+[
+  {
+     "id":"0001",
+     "type":"donut",
+     "name":"Cake",
+     "ppu":0.55,
+     "batters":{
+        "batter":[
+           {
+              "id":"1001",
+              "type":"Regular"
+           },
+           {
+              "id":"1002",
+              "type":"Chocolate"
+           },
+           {
+              "id":"1003",
+              "type":"Blueberry"
+           },
+           {
+              "id":"1004",
+              "type":"Devil's Food"
+           }
+        ]
+     },
+     "topping":[
+        {
+           "id":"5001",
+           "type":"None"
+        },
+        {
+           "id":"5002",
+           "type":"Glazed"
+        },
+        {
+           "id":"5005",
+           "type":"Sugar"
+        },
+        {
+           "id":"5007",
+           "type":"Powdered Sugar"
+        },
+        {
+           "id":"5006",
+           "type":"Chocolate with Sprinkles"
+        },
+        {
+           "id":"5003",
+           "type":"Chocolate"
+        },
+        {
+           "id":"5004",
+           "type":"Maple"
+        }
+     ]
+  },
+  {
+     "id":"0002",
+     "type":"donut",
+     "name":"Raised",
+     "ppu":0.55,
+     "batters":{
+        "batter":[
+           {
+              "id":"1001",
+              "type":"Regular"
+           }
+        ]
+     },
+     "topping":[
+        {
+           "id":"5001",
+           "type":"None"
+        },
+        {
+           "id":"5002",
+           "type":"Glazed"
+        },
+        {
+           "id":"5005",
+           "type":"Sugar"
+        },
+        {
+           "id":"5003",
+           "type":"Chocolate"
+        },
+        {
+           "id":"5004",
+           "type":"Maple"
+        }
+     ]
+  },
+  {
+     "id":"0003",
+     "type":"donut",
+     "name":"Old Fashioned",
+     "ppu":0.55,
+     "batters":{
+        "batter":[
+           {
+              "id":"1001",
+              "type":"Regular"
+           },
+           {
+              "id":"1002",
+              "type":"Chocolate"
+           }
+        ]
+     },
+     "topping":[
+        {
+           "id":"5001",
+           "type":"None"
+        },
+        {
+           "id":"5002",
+           "type":"Glazed"
+        },
+        {
+           "id":"5003",
+           "type":"Chocolate"
+        },
+        {
+           "id":"5004",
+           "type":"Maple"
+        }
+     ]
+  }
+]
diff --git a/main/tests/cypress/cypress/support/commands.js b/main/tests/cypress/cypress/support/commands.js
index 72ccf187a..3eda5e710 100644
--- a/main/tests/cypress/cypress/support/commands.js
+++ b/main/tests/cypress/cypress/support/commands.js
@@ -335,13 +335,13 @@ Cypress.Commands.add('dragAndDrop', (sourceSelector, targetSelector, position =
 Cypress.Commands.add(
   'loadAndVisitSampleJSONProject',
   (projectName, fixture) => {
+    const jsonText = JSON.stringify(fixture)
     cy.visitOpenRefine();
     cy.navigateTo('Create project');
     cy.get('#create-project-ui-source-selection-tabs > a')
       .contains('Clipboard')
       .click();
-
-    cy.get('textarea').invoke('val', fixture);
+    cy.get('textarea').invoke('val', jsonText);
     cy.get(
       '.create-project-ui-source-selection-tab-body.selected button.button-primary'
     )
diff --git a/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java b/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
index aa75b61c8..a44686a12 100644
--- a/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
+++ b/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
@@ -94,8 +94,7 @@ public class ImportingUtilitiesTests extends ImporterTest {
     @BeforeMethod
     public void setUp() {
         super.setUp();
-        ImportingManager.registerFormatGuesser("text", new TextFormatGuesser());
-        ImportingManager.registerFormatGuesser("text/line-based", new LineBasedFormatGuesser());
+        importFlowSettings();
     }

     @Test
@@ -594,7 +593,7 @@ public class ImportingUtilitiesTests extends ImporterTest {
     @Test
     public void testFormatForMultipleExcelFiles() throws IOException, FileUploadException {
         testMultipleFiles("dates", "excel95", ".xls",
-                ContentType.create("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), "binary");
+                ContentType.create("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), "binary/text/xml/xls/xlsx");
     }

     @Test
@@ -602,6 +601,11 @@ public class ImportingUtilitiesTests extends ImporterTest {
         testMultipleFiles("grid_small", "grid_small", ".json", ContentType.create("text/json"), "text/json");
     }

+    @Test
+    public void testFormatForMultipleODSFiles() throws IOException, FileUploadException {
+        testMultipleFiles("films", "films", ".ods", ContentType.create("application/vnd.oasis.opendocument.spreadsheet"), "text/xml/ods");
+    }
+
     private void testMultipleFiles(String file1, String file2, String fileSuffix, ContentType contentType, String expectedFormat)
             throws IOException, FileUploadException {

@@ -666,4 +670,65 @@ public class ImportingUtilitiesTests extends ImporterTest {
         assertEquals(expectedFormat, JSONUtilities.getArray(retrievalRecord, "files").get(0).get("format").asText());
         assertEquals(expectedFormat, JSONUtilities.getArray(retrievalRecord, "files").get(1).get("format").asText());
     }
+
+    private void importFlowSettings() {
+        // Register Format guessers
+        ImportingManager.registerFormatGuesser("text", new TextFormatGuesser());
+        ImportingManager.registerFormatGuesser("text/line-based", new LineBasedFormatGuesser());
+
+        // Extension to format mappings
+        ImportingManager.registerExtension(".txt", "text");
+        ImportingManager.registerExtension(".csv", "text/line-based/*sv");
+        ImportingManager.registerExtension(".tsv", "text/line-based/*sv");
+        ImportingManager.registerExtension(".xml", "text/xml");
+        ImportingManager.registerExtension(".atom", "text/xml");
+        ImportingManager.registerExtension(".json", "text/json");
+        ImportingManager.registerExtension(".js", "text/json");
+        ImportingManager.registerExtension(".xls", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerExtension(".xlsx", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerExtension(".ods", "text/xml/ods");
+        ImportingManager.registerExtension(".nt", "text/rdf/nt");
+        ImportingManager.registerExtension(".ntriples", "text/rdf/nt");
+        ImportingManager.registerExtension(".n3", "text/rdf/n3");
+        ImportingManager.registerExtension(".ttl", "text/rdf/ttl");
+        ImportingManager.registerExtension(".jsonld", "text/rdf/ld+json");
+        ImportingManager.registerExtension(".rdf", "text/rdf/xml");
+        ImportingManager.registerExtension(".marc", "text/marc");
+        ImportingManager.registerExtension(".mrc", "text/marc");
+        ImportingManager.registerExtension(".wiki", "text/wiki");
+
+        // Mime type to format mappings
+        ImportingManager.registerMimeType("text/plain", "text");
+        ImportingManager.registerMimeType("text/csv", "text/line-based/*sv");
+        ImportingManager.registerMimeType("text/x-csv", "text/line-based/*sv");
+        ImportingManager.registerMimeType("text/tab-separated-value", "text/line-based/*sv");
+        ImportingManager.registerMimeType("text/tab-separated-values", "text/line-based/*sv");
+        ImportingManager.registerMimeType("text/fixed-width", "text/line-based/fixed-width");
+        ImportingManager.registerMimeType("application/n-triples", "text/rdf/nt");
+        ImportingManager.registerMimeType("text/n3", "text/rdf/n3");
+        ImportingManager.registerMimeType("text/rdf+n3", "text/rdf/n3");
+        ImportingManager.registerMimeType("text/turtle", "text/rdf/ttl");
+        ImportingManager.registerMimeType("application/xml", "text/xml");
+        ImportingManager.registerMimeType("text/xml", "text/xml");
+        ImportingManager.registerMimeType("+xml", "text/xml"); // suffix will be tried only as fallback
+        ImportingManager.registerMimeType("application/rdf+xml", "text/rdf/xml");
+        ImportingManager.registerMimeType("application/ld+json", "text/rdf/ld+json");
+        ImportingManager.registerMimeType("application/atom+xml", "text/xml");
+        ImportingManager.registerMimeType("application/msexcel", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/x-msexcel", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/x-ms-excel", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/vnd.ms-excel", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/x-excel", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/xls", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/x-xls", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+                "binary/text/xml/xls/xlsx");
+        ImportingManager.registerMimeType("application/vnd.oasis.opendocument.spreadsheet", "text/xml/ods");
+        ImportingManager.registerMimeType("application/json", "text/json");
+        ImportingManager.registerMimeType("application/javascript", "text/json");
+        ImportingManager.registerMimeType("text/json", "text/json");
+        ImportingManager.registerMimeType("+json", "text/json"); // suffix will be tried only as fallback
+        ImportingManager.registerMimeType("application/marc", "text/marc");
+    }
 }

Fixes #6584

Changes proposed in this pull request:
- adds new file `donut-records.json` to Cypress fixtures.

Fixes #6584 add Cypress fixture for records


diff --git a/main/src/com/google/refine/importers/JsonImporter.java b/main/src/com/google/refine/importers/JsonImporter.java
index 1d8505dd9..ce8e0367e 100644
--- a/main/src/com/google/refine/importers/JsonImporter.java
+++ b/main/src/com/google/refine/importers/JsonImporter.java
@@ -44,6 +44,7 @@ import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.core.JsonParser.NumberType;
 import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.StreamReadFeature;
 import com.fasterxml.jackson.core.json.JsonReadFeature;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -92,7 +93,7 @@ public class JsonImporter extends TreeImportingParserBase {
                 ObjectNode firstFileRecord = fileRecords.get(0);
                 File file = ImportingUtilities.getFile(job, firstFileRecord);
                 JsonFactory factory = JsonFactory.builder().enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
-                        .enable(JsonReadFeature.ALLOW_YAML_COMMENTS).build();
+                        .enable(JsonReadFeature.ALLOW_YAML_COMMENTS).enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build();
                 JsonParser parser = factory.createParser(file);
                 parser.enable(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS);

@@ -103,6 +104,7 @@ public class JsonImporter extends TreeImportingParserBase {
                 }
             } catch (IOException e) {
                 logger.error("Error generating parser UI initialization data for JSON file", e);
+                JSONUtilities.safePut(options, "error", e.toString());
             }
         }

@@ -141,64 +143,52 @@ public class JsonImporter extends TreeImportingParserBase {
         return null;
     }

-    final static private JsonNode parseForPreview(JsonParser parser, PreviewParsingState state) {
-        try {
-            JsonToken token = parser.nextToken();
-            state.tokenCount++;
-            return parseForPreview(parser, state, token);
-        } catch (IOException e) {
-            return null;
-        }
+    final static private JsonNode parseForPreview(JsonParser parser, PreviewParsingState state) throws IOException {
+        JsonToken token = parser.nextToken();
+        state.tokenCount++;
+        return parseForPreview(parser, state, token);
     }

-    final static private ObjectNode parseObjectForPreview(JsonParser parser, PreviewParsingState state) {
+    final static private ObjectNode parseObjectForPreview(JsonParser parser, PreviewParsingState state) throws IOException {
         ObjectNode result = ParsingUtilities.mapper.createObjectNode();
         loop: while (state.tokenCount < PREVIEW_PARSING_LIMIT) {
-            try {
-                JsonToken token = parser.nextToken();
-                if (token == null) {
-                    break;
-                }
-                state.tokenCount++;
-
-                switch (token) {
-                    case FIELD_NAME:
-                        String fieldName = parser.getText();
-                        JsonNode fieldValue = parseForPreview(parser, state);
-                        JSONUtilities.safePut(result, fieldName, fieldValue);
-                        break;
-                    case END_OBJECT:
-                        break loop;
-                    default:
-                        break loop;
-                }
-            } catch (IOException e) {
+            JsonToken token = parser.nextToken();
+            if (token == null) {
                 break;
             }
+            state.tokenCount++;
+
+            switch (token) {
+                case FIELD_NAME:
+                    String fieldName = parser.getText();
+                    JsonNode fieldValue = parseForPreview(parser, state);
+                    JSONUtilities.safePut(result, fieldName, fieldValue);
+                    break;
+                case END_OBJECT:
+                    break loop;
+                default:
+                    break loop;
+            }
         }
         return result;
     }

-    final static private ArrayNode parseArrayForPreview(JsonParser parser, PreviewParsingState state) {
+    final static private ArrayNode parseArrayForPreview(JsonParser parser, PreviewParsingState state) throws IOException {
         ArrayNode result = ParsingUtilities.mapper.createArrayNode();
         loop: while (state.tokenCount < PREVIEW_PARSING_LIMIT) {
-            try {
-                JsonToken token = parser.nextToken();
-                if (token == null) {
-                    break;
-                }
-                state.tokenCount++;
-
-                switch (token) {
-                    case END_ARRAY:
-                        break loop;
-                    default:
-                        JsonNode element = parseForPreview(parser, state, token);
-                        result.add(element);
-                }
-            } catch (IOException e) {
+            JsonToken token = parser.nextToken();
+            if (token == null) {
                 break;
             }
+            state.tokenCount++;
+
+            switch (token) {
+                case END_ARRAY:
+                    break loop;
+                default:
+                    JsonNode element = parseForPreview(parser, state, token);
+                    result.add(element);
+            }
         }
         return result;
     }
diff --git a/main/webapp/modules/core/langs/translation-he.json b/main/webapp/modules/core/langs/translation-he.json
index db9a2b761..0688e7ef0 100644
--- a/main/webapp/modules/core/langs/translation-he.json
+++ b/main/webapp/modules/core/langs/translation-he.json
@@ -290,7 +290,7 @@
     "core-project/odf": "גיליון נתונים ODF",
     "core-project/custom-tabular": "מייצא טבלאי מותאם אישית…",
     "core-project/templating": "נוצרת תבנית…",
-    "core-project/json-invalid": "ה־JSON שהדבקת אינו תקני",
+    "core-project/json-invalid": "תבנית JSON שגויה: $1",
     "core-project/undo-history": "היסטוריית ביטולים אינסופית",
     "core-project/mistakes": "אל תחששו לבצע טעויות. כל שינוי שתעשו יוצג כאן, ותוכלו לבטל כל שינוי בכל עת.",
     "core-project/learn-more": "מידע נוסף «",
diff --git a/main/webapp/modules/core/langs/translation-ja.json b/main/webapp/modules/core/langs/translation-ja.json
index a9c31baad..45af7226b 100644
--- a/main/webapp/modules/core/langs/translation-ja.json
+++ b/main/webapp/modules/core/langs/translation-ja.json
@@ -331,7 +331,7 @@
     "core-project/clear-selection": "全ファセットの選択をクリア",
     "core-project/open": "開く",
     "core-project/facet-filter": "ファセット/フィルタ",
-    "core-project/json-invalid": "貼り付けたJSONは無効です",
+    "core-project/json-invalid": "JSON形式としては無効: $1",
     "core-project/last-op-er": "最後の操作でエラーが発生しました",
     "core-project/apply-operation": "操作履歴を適用",
     "core-project/sql-export": "SQL…",
@@ -917,5 +917,6 @@
     "core-dialogs/edit-name": "関数名",
     "core-dialogs/action": "アクション",
     "core-views/warning-function-name": "関数名を入力してください.",
-    "core-views/warning-delete-functions": "本当にこの関数を削除しますか?"
+    "core-views/warning-delete-functions": "本当にこの関数を削除しますか?",
+    "core-index-parser/store-blank-columns": "空白カラムも保存"
 }

diff --git a/main/tests/server/src/com/google/refine/importers/JsonImporterTests.java b/main/tests/server/src/com/google/refine/importers/JsonImporterTests.java
index 43dacc72d..726e1a4ce 100644
--- a/main/tests/server/src/com/google/refine/importers/JsonImporterTests.java
+++ b/main/tests/server/src/com/google/refine/importers/JsonImporterTests.java
@@ -33,6 +33,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 package com.google.refine.importers;

+import static org.testng.Assert.assertEquals;
+
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
@@ -40,6 +42,7 @@ import java.io.InputStream;
 import java.io.Serializable;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
@@ -109,7 +112,7 @@ public class JsonImporterTests extends ImporterTest {
         Row row = project.rows.get(0);
         Assert.assertNotNull(row);
         Assert.assertNotNull(row.getCell(1));
-        Assert.assertEquals(row.getCell(1).value, "Author 1, The");
+        assertEquals(row.getCell(1).value, "Author 1, The");
     }

     @Test
@@ -120,14 +123,27 @@ public class JsonImporterTests extends ImporterTest {
         Row row = project.rows.get(0);
         Assert.assertNotNull(row);
         Assert.assertNotNull(row.getCell(1));
-        Assert.assertEquals(row.getCell(1).value, "Author 1, The");
+        assertEquals(row.getCell(1).value, "Author 1, The");
     }

     @Test
-    public void canThrowError() {
+    public void canThrowError() throws IOException {
         String errJSON = getSampleWithError();
+        ObjectNode fileRecord = ParsingUtilities.mapper.createObjectNode();
+        JSONUtilities.safePut(fileRecord, "origin", "clipboard");
+        JSONUtilities.safePut(fileRecord, "declaredEncoding", StandardCharsets.UTF_8.toString());
+        JSONUtilities.safePut(fileRecord, "declaredMimeType", "text/json");
+        JSONUtilities.safePut(fileRecord, "format", "text/json");
+        JSONUtilities.safePut(fileRecord, "fileName", "(clipboard)");
+        File jsonFile = new File(job.getRawDataDir(), "test.json");
+        Files.write(jsonFile.toPath(), errJSON.getBytes(StandardCharsets.UTF_8));
+        JSONUtilities.safePut(fileRecord, "location", "test.json");
         ObjectNode options = SUT.createParserUIInitializationData(
-                job, new LinkedList<>(), "text/json");
+                job, List.of(fileRecord), "text/json");
+        assertEquals(options.get("error").asText(),
+                "com.fasterxml.jackson.core.JsonParseException: Unexpected character (';' (code 59)): was expecting comma to separate Object entries\n"
+                        +
+                        " at [Source: (File); line: 1, column: 11]");
         ArrayNode path = ParsingUtilities.mapper.createArrayNode();
         JSONUtilities.append(path, JsonImporter.ANONYMOUS);
         JSONUtilities.safePut(options, "recordPath", path);
@@ -137,7 +153,7 @@ public class JsonImporterTests extends ImporterTest {

         inputStream = new ByteArrayInputStream(errJSON.getBytes(StandardCharsets.UTF_8));
         ImportColumnGroup rootColumnGroup = new ImportColumnGroup();
-        List<Exception> exceptions = new ArrayList<Exception>();
+        List<Exception> exceptions = new ArrayList<>();

         SUT.parseOneFile(
                 project,
@@ -150,7 +166,7 @@ public class JsonImporterTests extends ImporterTest {
                 options,
                 exceptions);
         Assert.assertFalse(exceptions.isEmpty());
-        Assert.assertEquals("Unexpected character (';' (code 59)): was expecting comma to separate Object entries",
+        assertEquals("Unexpected character (';' (code 59)): was expecting comma to separate Object entries",
                 exceptions.get(0).getMessage());
     }

@@ -369,8 +385,8 @@ public class JsonImporterTests extends ImporterTest {
             }
             i++;
             if (i == 3) {
-                Assert.assertEquals(Token.Value, token);
-                Assert.assertEquals("field", parser.getFieldName());
+                assertEquals(Token.Value, token);
+                assertEquals("field", parser.getFieldName());
             }
         }

@@ -384,8 +400,8 @@ public class JsonImporterTests extends ImporterTest {
             }
             i++;
             if (i == 3) {
-                Assert.assertEquals(Token.StartEntity, token);
-                Assert.assertEquals(parser.getFieldName(), "field");
+                assertEquals(Token.StartEntity, token);
+                assertEquals(parser.getFieldName(), "field");
             }
         }

@@ -399,16 +415,16 @@ public class JsonImporterTests extends ImporterTest {
             }
             i++;
             if (i == 3) {
-                Assert.assertEquals(token, Token.StartEntity);
-                Assert.assertEquals(parser.getFieldName(), "field");
+                assertEquals(token, Token.StartEntity);
+                assertEquals(parser.getFieldName(), "field");
             }
             if (i == 4) {
-                Assert.assertEquals(token, Token.StartEntity);
-                Assert.assertEquals(parser.getFieldName(), JsonImporter.ANONYMOUS);
+                assertEquals(token, Token.StartEntity);
+                assertEquals(parser.getFieldName(), JsonImporter.ANONYMOUS);
             }
             if (i == 6) {
-                Assert.assertEquals(token, Token.StartEntity);
-                Assert.assertEquals(parser.getFieldName(), JsonImporter.ANONYMOUS);
+                assertEquals(token, Token.StartEntity);
+                assertEquals(parser.getFieldName(), JsonImporter.ANONYMOUS);
             }
         }
     }
@@ -428,9 +444,9 @@ public class JsonImporterTests extends ImporterTest {
             }
             i++;
             if (i == 3) {
-                Assert.assertEquals(Token.Value, token);
-                Assert.assertEquals("\tfield", parser.getFieldName());
-                Assert.assertEquals("\tvalue", parser.getFieldValue());
+                assertEquals(Token.Value, token);
+                assertEquals("\tfield", parser.getFieldName());
+                assertEquals("\tvalue", parser.getFieldValue());
             }
         }
     }

Fixes #6454

Changes proposed in this pull request:
- Add error reporting to JSON preview parse.

Add error reporting to JSON preview parse. Fixes #6454

def parser(log: str) -> dict[str, str]:
    import re
    import xml.etree.ElementTree as ET

    results: dict[str, str] = {}

    # Helper: merge statuses with precedence fail > skip > pass
    def merge_status(name: str, new_status: str):
        if new_status not in ("pass", "fail", "skip"):
            return
        old_status = results.get(name)
        if old_status is None:
            results[name] = new_status
            return
        if old_status == new_status:
            return
        # Precedence: fail > skip > pass
        priority = {"fail": 3, "skip": 2, "pass": 1}
        if priority[new_status] > priority[old_status]:
            results[name] = new_status

    # -----------------------
    # XML-based parsing pass
    # -----------------------

    # Non-greedy patterns for JUnit/TestNG XML blocks
    xml_pattern = re.compile(
        r"(<testng-results[\s\S]*?</testng-results>|<testsuite[\s\S]*?</testsuite>)",
        re.IGNORECASE,
    )

    for match in xml_pattern.finditer(log):
        xml_block = match.group(1)
        try:
            root = ET.fromstring(xml_block)
        except ET.ParseError:
            # Skip this block but continue with others
            continue

        tag_lower = root.tag.lower()

        # Handle TestNG summary reports (<testng-results>)
        if tag_lower.endswith("testng-results"):
            # Look for all test-method elements anywhere
            for tm in root.findall(".//test-method"):
                # Skip configuration methods
                if tm.attrib.get("is-config", "false").lower() == "true":
                    continue
                name = tm.attrib.get("name", "")
                if not name:
                    continue
                # Try to get class name from surrounding <class> element
                class_elem = tm.find("..")
                class_name = ""
                # xml.etree doesn't support parent lookup directly, so search ancestors:
                # We instead search for the nearest <class> ancestor manually:
            # Since parent lookup is tricky, fall back to the attribute 'class' on <test-method> if present
            # or to a surrounding <class> element found via xpath with name match, which is usually enough.
            # Re-parse with a simpler loop using suites/tests/classes for better class name resolution.
            for suite in root.findall(".//suite"):
                for test in suite.findall(".//test"):
                    for clazz in test.findall(".//class"):
                        class_name = clazz.attrib.get("name", "")
                        for tm in clazz.findall("test-method"):
                            if tm.attrib.get("is-config", "false").lower() == "true":
                                continue
                            m_name = tm.attrib.get("name", "")
                            if not m_name:
                                continue
                            fq_name = f"{class_name}.{m_name}" if class_name else m_name
                            status_attr = tm.attrib.get("status", "").upper()
                            # Map TestNG status to our statuses
                            if status_attr == "PASS":
                                status = "pass"
                            elif status_attr in ("FAIL", "FAILURE"):
                                status = "fail"
                            elif status_attr == "SKIP":
                                status = "skip"
                            else:
                                # Fallback: if there is an exception element, mark as fail; else pass
                                if tm.find(".//exception") is not None:
                                    status = "fail"
                                else:
                                    status = "pass"
                            merge_status(fq_name, status)
            continue

        # Handle JUnit-style <testsuite> reports
        if tag_lower.endswith("testsuite"):
            for tc in root.findall(".//testcase"):
                class_name = tc.attrib.get("classname", "")
                name = tc.attrib.get("name", "")
                if not name:
                    continue
                fq_name = f"{class_name}.{name}" if class_name else name

                # Determine status via children
                status = "pass"
                if tc.find("skipped") is not None:
                    status = "skip"
                elif tc.find("failure") is not None or tc.find("error") is not None:
                    status = "fail"

                merge_status(fq_name, status)

    # ---------------------------
    # Text-based parsing fallback
    # ---------------------------

    # Compile regexes for common frameworks

    # pytest: path::test_name STATUS
    pytest_re = re.compile(
        r"^(?P<name>\S+::\S+)\s+"
        r"(?P<status>PASSED|FAILED|SKIPPED|ERROR|XFAILED|XPASSED)\b"
    )

    # unittest / nose: ClassName.test_method ... ok/FAIL/ERROR/skipped
    unittest_re = re.compile(
        r"^(?P<class>[A-Za-z_]\w*)\.(?P<method>[A-Za-z_]\w*)\s+\.\.\.\s+"
        r"(?P<status>ok|FAIL|ERROR|skipped)\b"
    )

    # go test: --- STATUS: TestName
    gotest_re = re.compile(
        r"^---\s+(?P<status>PASS|FAIL|SKIP):\s+(?P<name>\S+)"
    )

    # Jest-like symbol lines: ✓/✕/○ test name
    jest_symbol_re = re.compile(r"^\s*([✓✕○])\s+(?P<name>.+)")

    # Map various raw statuses to our canonical ones
    def map_status(raw: str) -> str | None:
        r = raw.upper()
        if r in ("PASSED", "OK", "XPASSED", "PASS"):
            return "pass"
        if r in ("FAILED", "FAIL", "ERROR", "XFAILED"):
            return "fail"
        if r in ("SKIPPED", "SKIP", "SKIPPED (EXPECTED FAILURE)"):
            return "skip"
        return None

    for line in log.splitlines():
        line = line.rstrip("\n")

        # pytest
        m = pytest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # unittest
        m = unittest_re.match(line)
        if m:
            name = f"{m.group('class')}.{m.group('method')}"
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # go test
        m = gotest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # Jest symbols
        m = jest_symbol_re.match(line)
        if m:
            symbol = m.group(1)
            name = m.group("name").strip()
            if symbol == "✓":
                status = "pass"
            elif symbol == "✕":
                status = "fail"
            elif symbol == "○":
                status = "skip"
            else:
                status = None
            if status:
                merge_status(name, status)
            continue

    return results

export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq nodejs npm
./refine build

find . -maxdepth 6 -path '*surefire-reports/*.xml' -print0 | xargs -0 -I {} sh -c 'echo "===== {} ====="; cat {}'


diff --git a/extensions/gdata/pom.xml b/extensions/gdata/pom.xml
index 59375016a..2ed919c1b 100644
--- a/extensions/gdata/pom.xml
+++ b/extensions/gdata/pom.xml
@@ -84,7 +84,7 @@
     <dependency>
       <groupId>com.google.apis</groupId>
       <artifactId>google-api-services-drive</artifactId>
-      <version>v3-rev20231120-2.0.0</version>
+      <version>v3-rev20231128-2.0.0</version>
     </dependency>
     <dependency>
       <groupId>com.google.apis</groupId>
diff --git a/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java b/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java
index 353d3a3f5..a54c8eaf8 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizer.java
@@ -22,7 +22,7 @@ public class FileNameScrutinizer extends EditScrutinizer {
     // see https://commons.wikimedia.org/wiki/Commons:File_naming
     public static final int maxFileNameBytes = 240;
     public static final Pattern forbiddenFileNameChars = Pattern.compile(
-            ".*([^ %!\"$&'()*,\\-./\\d:;=?@\\p{L}\\p{Mc}\\\\^_`~\\x80-\\xFF+]|%[0-9A-Fa-f]{2}|&[A-Za-z0-9\\x80-\\xff]+;|&#[0-9]+;|&#x[0-9A-Fa-f]+;).*");
+            ".*([^ %!\"$&'()*,\\-./\\d:;=?@\\p{L}\\p{M}\\p{N}\\\\^_`~\\x80-\\xFF+]|%[0-9A-Fa-f]{2}|&[A-Za-z0-9\\x80-\\xff]+;|&#[0-9]+;|&#x[0-9A-Fa-f]+;).*");

     public static final String duplicateFileNamesInBatchType = "duplicate-file-names-in-batch";
     public static final String fileNamesAlreadyExistOnWikiType = "file-names-already-exist-on-wiki";
@@ -98,7 +98,7 @@ public class FileNameScrutinizer extends EditScrutinizer {
             // Invalid characters
             Matcher matcher = forbiddenFileNameChars.matcher(fileName);
             if (matcher.matches()) {
-                QAWarning issue = new QAWarning(invalidCharactersInFileNameType, null, QAWarning.Severity.CRITICAL,
+                QAWarning issue = new QAWarning(invalidCharactersInFileNameType, null, QAWarning.Severity.IMPORTANT,
                         1);
                 issue.setProperty("example_filename", fileName);
                 issue.setProperty("invalid_character", matcher.group(1));

diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java
index abb1da6c4..5ced94b25 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/scrutinizers/FileNameScrutinizerTest.java
@@ -78,6 +78,16 @@ public class FileNameScrutinizerTest extends ScrutinizerTest {
         assertNoWarningRaised();
     }

+    @Test
+    public void testValidCharactersInFilenameNonAscii() {
+        MediaInfoEdit edit = new MediaInfoEditBuilder(TestingData.newMidA)
+                .addFileName("সমাচার দর্পণ - ৮ অক্টোবর ১৮৩৬.pdf")
+                .build();
+
+        scrutinize(edit);
+        assertNoWarningRaised();
+    }
+
     @Test()
     public void testInvalidCharactersInFilenameTab() throws IOException, MediaWikiApiErrorException {
         MediaInfoEdit edit = new MediaInfoEditBuilder(TestingData.newMidA)

I can't find authoritative documentation about this so this is pretty much example driven. To reflect that, I have lowered the severity of the check so that users are not prevented from uploading if the check incorrectly fails.

Closes #5656.

Widen regex of allowed characters in Commons filenames.


diff --git a/main/src/com/google/refine/util/HttpClient.java b/main/src/com/google/refine/util/HttpClient.java
index 6cdada46f..4ae8bfb71 100644
--- a/main/src/com/google/refine/util/HttpClient.java
+++ b/main/src/com/google/refine/util/HttpClient.java
@@ -28,9 +28,6 @@
 package com.google.refine.util;

 import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
-import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.List;
@@ -244,20 +241,13 @@ public class HttpClient {
     }

     public String getResponse(String urlString, Header[] headers, HttpClientResponseHandler<String> responseHandler) throws IOException {
-        try {
-            // Use of URL constructor below is purely to get additional error checking to mimic
-            // previous behavior for the tests.
-            new URL(urlString).toURI();
-        } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
-            return null;
-        }

         HttpGet httpGet = new HttpGet(urlString);

         if (headers != null && headers.length > 0) {
             httpGet.setHeaders(headers);
         }
-        httpGet.setConfig(defaultRequestConfig); // FIXME: Redundant? already includes in client builder
+        httpGet.setConfig(defaultRequestConfig); // FIXME: Redundant? already included in client builder
         return httpClient.execute(httpGet, responseHandler);
     }

diff --git a/main/webapp/modules/core/scripts/util/dialog.js b/main/webapp/modules/core/scripts/util/dialog.js
index dca9275e4..db9dbcc9d 100644
--- a/main/webapp/modules/core/scripts/util/dialog.js
+++ b/main/webapp/modules/core/scripts/util/dialog.js
@@ -83,7 +83,7 @@ DialogSystem.showDialog = function(elmt, onCancel) {
   }

   elmt.attr("tabindex", -1);
-  elmt.focus();
+  elmt.trigger('focus');

   return level;
 };

diff --git a/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java b/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java
index 095dca2cc..9a148037c 100644
--- a/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/column/ColumnAdditionByFetchingURLsOperationTests.java
@@ -51,6 +51,7 @@ import org.testng.annotations.Test;

 import com.google.refine.RefineTest;
 import com.google.refine.browsing.EngineConfig;
+import com.google.refine.expr.EvalError;
 import com.google.refine.expr.ExpressionUtils;
 import com.google.refine.model.AbstractOperation;
 import com.google.refine.model.Cell;
@@ -193,7 +194,7 @@ public class ColumnAdditionByFetchingURLsOperationTests extends RefineTest {
             server.enqueue(new MockResponse());

             Row row0 = new Row(2);
-            row0.setCell(0, new Cell("auinrestrsc", null)); // malformed -> null
+            row0.setCell(0, new Cell("auinrestrsc", null)); // malformed -> error
             project.rows.add(row0);
             Row row1 = new Row(2);
             row1.setCell(0, new Cell(url.toString(), null)); // fine
@@ -217,8 +218,8 @@ public class ColumnAdditionByFetchingURLsOperationTests extends RefineTest {

             int newCol = project.columnModel.getColumnByName("junk").getCellIndex();
             // Inspect rows
-            Assert.assertEquals(project.rows.get(0).getCellValue(newCol), null);
-            Assert.assertTrue(project.rows.get(1).getCellValue(newCol) != null);
+            Assert.assertTrue(project.rows.get(0).getCellValue(newCol) instanceof EvalError);
+            Assert.assertNotNull(project.rows.get(1).getCellValue(newCol));
             Assert.assertTrue(ExpressionUtils.isError(project.rows.get(2).getCellValue(newCol)));
         }
     }

Fixes #6137

Changes proposed in this pull request:
- Instead of swallowing URL/URI syntax errors and returning `null` to the user, return an `EvalError` object with the message from the exception
- change the test behavior to match

Return error, not null, on bad URL. Fixes #6137.


diff --git a/extensions/database/src/com/google/refine/extension/database/DatabaseConfiguration.java b/extensions/database/src/com/google/refine/extension/database/DatabaseConfiguration.java
index c9b8976bd..4f4a14bef 100644
--- a/extensions/database/src/com/google/refine/extension/database/DatabaseConfiguration.java
+++ b/extensions/database/src/com/google/refine/extension/database/DatabaseConfiguration.java
@@ -29,6 +29,9 @@

 package com.google.refine.extension.database;

+import java.net.URI;
+import java.net.URISyntaxException;
+
 public class DatabaseConfiguration {

     private String connectionName;
@@ -123,4 +126,17 @@ public class DatabaseConfiguration {
                 + databaseSchema + ", useSSL=" + useSSL + "]";
     }

+    public URI toURI() {
+        try {
+            return new URI(
+                    "jdbc:" + databaseType.toLowerCase(),
+                    databaseHost + ((databasePort == 0) ? "" : (":" + databasePort)),
+                    "/" + databaseName,
+                    useSSL ? "useSSL=true" : null,
+                    null
+            );
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
 }
diff --git a/extensions/database/src/com/google/refine/extension/database/mariadb/MariaDBConnectionManager.java b/extensions/database/src/com/google/refine/extension/database/mariadb/MariaDBConnectionManager.java
index 422735d1c..93d63c00e 100644
--- a/extensions/database/src/com/google/refine/extension/database/mariadb/MariaDBConnectionManager.java
+++ b/extensions/database/src/com/google/refine/extension/database/mariadb/MariaDBConnectionManager.java
@@ -134,7 +134,7 @@ public class MariaDBConnectionManager {

             Class.forName(type.getClassPath());
             DriverManager.setLoginTimeout(10);
-            String dbURL = getDatabaseUrl(databaseConfiguration);
+            String dbURL = databaseConfiguration.toURI().toString();
             connection = DriverManager.getConnection(dbURL, databaseConfiguration.getDatabaseUser(),
                     databaseConfiguration.getDatabasePassword());

@@ -162,14 +162,6 @@ public class MariaDBConnectionManager {
                 logger.warn("Non-Managed connection could not be closed. Whoops!", e);
             }
         }
-
     }

-    private static String getDatabaseUrl(DatabaseConfiguration dbConfig) {
-
-        int port = dbConfig.getDatabasePort();
-        return "jdbc:" + dbConfig.getDatabaseType().toLowerCase() + "://" + dbConfig.getDatabaseHost()
-                + ((port == 0) ? "" : (":" + port)) + "/" + dbConfig.getDatabaseName();
-
-    }
 }
diff --git a/extensions/database/src/com/google/refine/extension/database/mysql/MySQLConnectionManager.java b/extensions/database/src/com/google/refine/extension/database/mysql/MySQLConnectionManager.java
index f4a7ce5dd..4fea86a90 100644
--- a/extensions/database/src/com/google/refine/extension/database/mysql/MySQLConnectionManager.java
+++ b/extensions/database/src/com/google/refine/extension/database/mysql/MySQLConnectionManager.java
@@ -129,7 +129,7 @@ public class MySQLConnectionManager {
                     return connection;
                 }
             }
-            String dbURL = getDatabaseUrl(databaseConfiguration);
+            String dbURL = databaseConfiguration.toURI().toString();
             Class.forName(type.getClassPath());

             // logger.info("*** type.getClassPath() ::{}, {}**** ", type.getClassPath());
@@ -166,11 +166,4 @@ public class MySQLConnectionManager {

     }

-    private String getDatabaseUrl(DatabaseConfiguration dbConfig) {
-
-        int port = dbConfig.getDatabasePort();
-        return "jdbc:" + dbConfig.getDatabaseType() + "://" + dbConfig.getDatabaseHost()
-                + ((port == 0) ? "" : (":" + port)) + "/" + dbConfig.getDatabaseName() + "?useSSL=" + dbConfig.isUseSSL();
-
-    }
 }
diff --git a/extensions/database/src/com/google/refine/extension/database/pgsql/PgSQLConnectionManager.java b/extensions/database/src/com/google/refine/extension/database/pgsql/PgSQLConnectionManager.java
index bea76d091..1637c222e 100644
--- a/extensions/database/src/com/google/refine/extension/database/pgsql/PgSQLConnectionManager.java
+++ b/extensions/database/src/com/google/refine/extension/database/pgsql/PgSQLConnectionManager.java
@@ -136,7 +136,7 @@ public class PgSQLConnectionManager {

             Class.forName(type.getClassPath());
             DriverManager.setLoginTimeout(10);
-            String dbURL = getDatabaseUrl(databaseConfiguration);
+            String dbURL = databaseConfiguration.toURI().toString();
             connection = DriverManager.getConnection(dbURL, databaseConfiguration.getDatabaseUser(),
                     databaseConfiguration.getDatabasePassword());

@@ -165,11 +165,4 @@ public class PgSQLConnectionManager {

     }

-    private static String getDatabaseUrl(DatabaseConfiguration dbConfig) {
-
-        int port = dbConfig.getDatabasePort();
-        return "jdbc:" + dbConfig.getDatabaseType().toLowerCase() + "://" + dbConfig.getDatabaseHost()
-                + ((port == 0) ? "" : (":" + port)) + "/" + dbConfig.getDatabaseName();
-
-    }
 }
diff --git a/extensions/database/src/com/google/refine/extension/database/sqlite/SQLiteConnectionManager.java b/extensions/database/src/com/google/refine/extension/database/sqlite/SQLiteConnectionManager.java
index 5b9b4cfcd..7d42e002a 100644
--- a/extensions/database/src/com/google/refine/extension/database/sqlite/SQLiteConnectionManager.java
+++ b/extensions/database/src/com/google/refine/extension/database/sqlite/SQLiteConnectionManager.java
@@ -35,6 +35,8 @@ import com.google.refine.extension.database.SQLType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;

+import java.net.URI;
+import java.net.URISyntaxException;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.SQLException;
@@ -66,7 +68,12 @@ public class SQLiteConnectionManager {
     }

     public static String getDatabaseUrl(DatabaseConfiguration dbConfig) {
-        return "jdbc:" + dbConfig.getDatabaseType().toLowerCase() + ":" + dbConfig.getDatabaseName();
+        try {
+            URI uri = new URI("jdbc:" + dbConfig.getDatabaseType().toLowerCase(), dbConfig.getDatabaseName(), null);
+            return uri.toASCIIString();
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException(e);
+        }
     }

     /**
diff --git a/main/src/com/google/refine/sorting/StringCriterion.java b/main/src/com/google/refine/sorting/StringCriterion.java
index 9abcc6f40..8d314ce4b 100644
--- a/main/src/com/google/refine/sorting/StringCriterion.java
+++ b/main/src/com/google/refine/sorting/StringCriterion.java
@@ -54,11 +54,11 @@ public class StringCriterion extends Criterion {
         super();
         collator = Collator.getInstance();
         collator.setDecomposition(Collator.FULL_DECOMPOSITION);
-        collator.setStrength(Collator.SECONDARY);
     }

     @Override
     public KeyMaker createKeyMaker() {
+        collator.setStrength(caseSensitive ? Collator.IDENTICAL : Collator.SECONDARY);
         return new KeyMaker() {

             @Override

diff --git a/extensions/database/tests/src/com/google/refine/extension/database/DatabaseConfigurationTest.java b/extensions/database/tests/src/com/google/refine/extension/database/DatabaseConfigurationTest.java
new file mode 100644
index 000000000..5a571e8f0
--- /dev/null
+++ b/extensions/database/tests/src/com/google/refine/extension/database/DatabaseConfigurationTest.java
@@ -0,0 +1,21 @@
+package com.google.refine.extension.database;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+public class DatabaseConfigurationTest {
+
+    @Test
+    public void testToURI() {
+        DatabaseConfiguration config = new DatabaseConfiguration();
+        config.setDatabaseType("mysql");
+        config.setDatabaseHost("my.host");
+        // maliciously crafted database name which attempts to enable local file reads for an exploit
+        config.setDatabaseName("test?allowLoadLocalInfile=true#");
+
+        String url = config.toURI().toString();
+        // the database name is escaped, preventing the exploit
+        assertEquals(url, "jdbc:mysql://my.host/test%3FallowLoadLocalInfile=true%23");
+    }
+}
diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index c81d6360a..ff8194229 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -16,7 +16,7 @@
         "lint": "prettier --check . && eslint ."
     },
     "dependencies": {
-        "cypress": "13.1.0",
+        "cypress": "13.2.0",
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 208e0a6ab..f5f0020d7 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -116,10 +116,10 @@
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz"
   integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==

-"@types/node@*", "@types/node@^16.18.39":
-  version "16.18.39"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.39.tgz#aa39a1a87a40ef6098ee69689a1acb0c1b034832"
-  integrity sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ==
+"@types/node@*", "@types/node@^18.17.5":
+  version "18.17.15"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.15.tgz#31301a273b9ca7d568fe6d1c35ae52e0fb3f8d6a"
+  integrity sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==

 "@types/sinonjs__fake-timers@8.1.1":
   version "8.1.1"
@@ -431,14 +431,14 @@ cypress-file-upload@^5.0.8:
   resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
   integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==

-cypress@13.1.0:
-  version "13.1.0"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.1.0.tgz#18f268e66662cd91b1766db18bd1f63a66592205"
-  integrity sha512-LUKxCYlB973QBFls1Up4FAE9QIYobT+2I8NvvAwMfQS2YwsWbr6yx7y9hmsk97iqbHkKwZW3MRjoK1RToBFVdQ==
+cypress@13.2.0:
+  version "13.2.0"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.2.0.tgz#10f73d06a0764764ffbb903a31e96e2118dcfc1d"
+  integrity sha512-AvDQxBydE771GTq0TR4ZUBvv9m9ffXuB/ueEtpDF/6gOcvFR96amgwSJP16Yhqw6VhmwqspT5nAGzoxxB+D89g==
   dependencies:
     "@cypress/request" "^3.0.0"
     "@cypress/xvfb" "^1.2.4"
-    "@types/node" "^16.18.39"
+    "@types/node" "^18.17.5"
     "@types/sinonjs__fake-timers" "8.1.1"
     "@types/sizzle" "^2.3.2"
     arch "^2.2.0"
diff --git a/main/tests/server/src/com/google/refine/operations/row/RowReorderOperationTests.java b/main/tests/server/src/com/google/refine/operations/row/RowReorderOperationTests.java
index 87cc866ea..6a460b934 100644
--- a/main/tests/server/src/com/google/refine/operations/row/RowReorderOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/row/RowReorderOperationTests.java
@@ -63,7 +63,9 @@ public class RowReorderOperationTests extends RefineTest {
                         "8,b\n" +
                         ",d\n" +
                         "2,f\n" +
-                        "1,h\n");
+                        "1,h\n" +
+                        "9,F\n" +
+                        "10,f\n");
     }

     @AfterMethod
@@ -81,10 +83,48 @@ public class RowReorderOperationTests extends RefineTest {
         Process process = op.createProcess(project, new Properties());
         process.performImmediate();

-        Assert.assertEquals("h", project.rows.get(0).cells.get(1).value);
-        Assert.assertEquals("f", project.rows.get(1).cells.get(1).value);
-        Assert.assertEquals("b", project.rows.get(2).cells.get(1).value);
-        Assert.assertEquals("d", project.rows.get(3).cells.get(1).value);
+        Assert.assertEquals(project.rows.get(0).cells.get(1).value, "h");
+        Assert.assertEquals(project.rows.get(1).cells.get(1).value, "f");
+        Assert.assertEquals(project.rows.get(2).cells.get(1).value, "b");
+        Assert.assertEquals(project.rows.get(3).cells.get(1).value, "F");
+        Assert.assertEquals(project.rows.get(4).cells.get(1).value, "f");
+        Assert.assertEquals(project.rows.get(5).cells.get(1).value, "d");
+    }
+
+    @Test
+    public void testReverseSort() throws Exception {
+        String sortingJson = "{\"criteria\":[{\"column\":\"key\",\"valueType\":\"number\",\"reverse\":true,\"blankPosition\":-1,\"errorPosition\":1}]}";
+        SortingConfig sortingConfig = SortingConfig.reconstruct(sortingJson);
+        project.rows.get(1).cells.set(0, new Cell("", null));
+        AbstractOperation op = new RowReorderOperation(
+                Mode.RowBased, sortingConfig);
+        Process process = op.createProcess(project, new Properties());
+        process.performImmediate();
+
+        Assert.assertEquals(project.rows.get(5).cells.get(1).value, "h");
+        Assert.assertEquals(project.rows.get(4).cells.get(1).value, "f");
+        Assert.assertEquals(project.rows.get(3).cells.get(1).value, "b");
+        Assert.assertEquals(project.rows.get(2).cells.get(1).value, "F");
+        Assert.assertEquals(project.rows.get(1).cells.get(1).value, "f");
+        Assert.assertEquals(project.rows.get(0).cells.get(1).value, "d"); // controlled by blankPosition, not reverse
+    }
+
+    @Test
+    public void testStringSort() throws Exception {
+        String sortingJson = "{\"criteria\":[{\"column\":\"first\",\"valueType\":\"string\",\"reverse\":false,\"blankPosition\":2,\"errorPosition\":1,\"caseSensitive\":true}]}";
+        SortingConfig sortingConfig = SortingConfig.reconstruct(sortingJson);
+        project.rows.get(1).cells.set(0, new Cell("", null));
+        AbstractOperation op = new RowReorderOperation(
+                Mode.RowBased, sortingConfig);
+        Process process = op.createProcess(project, new Properties());
+        process.performImmediate();
+
+        Assert.assertEquals(project.rows.get(0).cells.get(1).value, "b");
+        Assert.assertEquals(project.rows.get(1).cells.get(1).value, "d");
+        Assert.assertEquals(project.rows.get(2).cells.get(1).value, "f");
+        Assert.assertEquals(project.rows.get(3).cells.get(1).value, "f");
+        Assert.assertEquals(project.rows.get(4).cells.get(1).value, "F");
+        Assert.assertEquals(project.rows.get(5).cells.get(1).value, "h");
     }

     @Test

Fixes #6047

Changes proposed in this pull request:
- Fix the case sensitive parameter being ignored for string sorts
- Correct parameter ordering for asserts in tests
- Add tests for reversed sort parameter, empty item position, and string sort

Fix case sensitive string sort. Fixes #6047