diff --git a/src/main/java/com/thealgorithms/devutils/searches/MatrixSearchAlgorithm.java b/src/main/java/com/thealgorithms/devutils/searches/MatrixSearchAlgorithm.java
new file mode 100644
index 00000000..f102bd5c
--- /dev/null
+++ b/src/main/java/com/thealgorithms/devutils/searches/MatrixSearchAlgorithm.java
@@ -0,0 +1,16 @@
+package com.thealgorithms.devutils.searches;
+
+/**
+ * The common interface of most searching algorithms that search in matrixes.
+ *
+ * @author Aitor Fidalgo (https://github.com/aitorfi)
+ */
+public interface MatrixSearchAlgorithm {
+    /**
+     * @param key is an element which should be found
+     * @param matrix is a matrix where the element should be found
+     * @param <T> Comparable type
+     * @return array containing the first found coordinates of the element
+     */
+    <T extends Comparable<T>> int[] find(T matrix[][], T key);
+}
diff --git a/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java b/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java
index e157dff2..3a672d7c 100644
--- a/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java
+++ b/src/main/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearch.java
@@ -1,37 +1,46 @@
 package com.thealgorithms.searches;

-// The search is for any array which is sorted row and column-wise too. For ex :
-// int[][] arr = { {10, 20, 30, 40},
-//                 {15, 25, 35, 45},
-//                 {18, 28, 38, 48},
-//                 {21, 31, 41, 51}
-//                };
-// This array is sorted in both row and column manner.
-// In this two pointers are taken, the first points to the 0th row and the second one points to end column, and then the
-// element corresponding to the pointers placed in the array is compared with the target that either its equal, greater or
-// smaller than the target. If the element is equal to the target, the co-ordinates of that element is returned i.e. an
-// array of the two pointers will be returned, else if the target is greater than corresponding element then the pointer
-// pointing to the 0th row will be incremented by 1, else if the target is lesser than the corresponding element then the
-// pointer pointing to the end column will be decremented by 1. And if the element doesn't exist in the array, an array
-// {-1, -1} will be returned.
+import com.thealgorithms.devutils.searches.MatrixSearchAlgorithm;

-public class RowColumnWiseSorted2dArrayBinarySearch {
+/**
+ * The search is for any array which is sorted row and column-wise too. For ex :
+ * {{10, 20, 30, 40},
+ *  {15, 25, 35, 45},
+ *  {18, 28, 38, 48},
+ *  {21, 31, 41, 51}}
+ *
+ * This array is sorted in both row and column manner.
+ * In this two pointers are taken, the first points to the 0th row and the second one points to end column, and then the
+ * element corresponding to the pointers placed in the array is compared with the target that either its equal, greater or
+ * smaller than the target. If the element is equal to the target, the co-ordinates of that element is returned i.e. an
+ * array of the two pointers will be returned, else if the target is greater than corresponding element then the pointer
+ * pointing to the 0th row will be incremented by 1, else if the target is lesser than the corresponding element then the
+ * pointer pointing to the end column will be decremented by 1. And if the element doesn't exist in the array, an array
+ * {-1, -1} will be returned.
+ */
+public class RowColumnWiseSorted2dArrayBinarySearch
+  implements MatrixSearchAlgorithm {

-    public static int[] search(int[][] matrix, int target) {
+  @Override
+  public <T extends Comparable<T>> int[] find(T[][] matrix, T key) {
+    return search(matrix, key);
+  }

-        int rowPointer = 0;                 //The pointer at 0th row
-        int colPointer = matrix.length-1;   //The pointer at end column
+  public static <T extends Comparable<T>> int[] search(T[][] matrix, T target) {
+    int rowPointer = 0; //The pointer at 0th row
+    int colPointer = matrix.length - 1; //The pointer at end column

-        while (rowPointer < matrix.length && colPointer >= 0){
+    while (rowPointer < matrix.length && colPointer >= 0) {
+      int comp = target.compareTo(matrix[rowPointer][colPointer]);

-            if (matrix[rowPointer][colPointer] == target)
-                return new int[] {rowPointer, colPointer};
-
-            else if (matrix[rowPointer][colPointer] < target)
-                rowPointer++;               //Incrementing the row pointer if the target is greater
-            else
-                colPointer--;               //Decrementing the column pointer if the target is lesser
-        }
-        return new int[] {-1, -1};          //The not found condition
+      if (comp == 0) {
+        return new int[] { rowPointer, colPointer };
+      } else if (comp > 0) {
+        rowPointer++; //Incrementing the row pointer if the target is greater
+      } else {
+        colPointer--; //Decrementing the column pointer if the target is lesser
+      }
     }
+    return new int[] { -1, -1 }; //The not found condition
+  }
 }

diff --git a/src/test/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearchTest.java b/src/test/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearchTest.java
index 3343c1ce..b15b53c3 100644
--- a/src/test/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearchTest.java
+++ b/src/test/java/com/thealgorithms/searches/RowColumnWiseSorted2dArrayBinarySearchTest.java
@@ -1,124 +1,113 @@
 package com.thealgorithms.searches;

-import org.junit.jupiter.api.Test;
-
-import java.util.*;
-
 import static org.junit.jupiter.api.Assertions.assertEquals;

+import org.junit.jupiter.api.Test;

 public class RowColumnWiseSorted2dArrayBinarySearchTest {

-    @Test
-    // valid test case
-    public void rowColumnSorted2dArrayBinarySearchTestMiddle() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 35;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {1,2};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(1, ans[0]);
-        assertEquals(2, ans[1]);
-    }
-
-    @Test
-    // valid test case
-    public void rowColumnSorted2dArrayBinarySearchTestSide() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 48;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {2,3};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(2, ans[0]);
-        assertEquals(3, ans[1]);
-    }
+  @Test
+  public void rowColumnSorted2dArrayBinarySearchTestMiddle() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 35;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { 1, 2 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }

-    @Test
-    // valid test case
-    public void rowColumnSorted2dArray_BinarySearchTestUpper() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 20;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {0,1};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(0, ans[0]);
-        assertEquals(1, ans[1]);
-    }
+  @Test
+  public void rowColumnSorted2dArrayBinarySearchTestSide() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 48;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { 2, 3 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }

-    @Test
-    // valid test case
-    public void rowColumnSorted2dArray_BinarySearchTestUpperSide() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 40;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {0,3};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(0, ans[0]);
-        assertEquals(3, ans[1]);
-    }
+  @Test
+  public void rowColumnSorted2dArray_BinarySearchTestUpper() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 20;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { 0, 1 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }

-    @Test
-    // valid test case
-    public void rowColumnSorted2dArray_BinarySearchTestLower() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 31;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {3,1};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(3, ans[0]);
-        assertEquals(1, ans[1]);
-    }
+  @Test
+  public void rowColumnSorted2dArray_BinarySearchTestUpperSide() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 40;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { 0, 3 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }

-    @Test
-    // valid test case
-    public void rowColumnSorted2dArray_BinarySearchTestLowerSide() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 51;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {3,3};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(3, ans[0]);
-        assertEquals(3, ans[1]);
-    }
+  @Test
+  public void rowColumnSorted2dArray_BinarySearchTestLower() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 31;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { 3, 1 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }

-    @Test
-    // valid test case
-    public void rowColumnSorted2dArray_BinarySearchTestNotFound() {
-        int[][] arr = { {10, 20, 30, 40},
-                        {15, 25, 35, 45},
-                        {18, 28, 38, 48},
-                        {21, 31, 41, 51}
-                      };
-        int target = 101;
-        int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
-        int[] expected = {-1,-1};
-        System.out.println(Arrays.toString(ans));
-        assertEquals(-1, ans[0]);
-        assertEquals(-1, ans[1]);
-    }
+  @Test
+  public void rowColumnSorted2dArray_BinarySearchTestLowerSide() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 51;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { 3, 3 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }

+  @Test
+  public void rowColumnSorted2dArray_BinarySearchTestNotFound() {
+    Integer[][] arr = {
+      { 10, 20, 30, 40 },
+      { 15, 25, 35, 45 },
+      { 18, 28, 38, 48 },
+      { 21, 31, 41, 51 },
+    };
+    Integer target = 101;
+    int[] ans = RowColumnWiseSorted2dArrayBinarySearch.search(arr, target);
+    int[] expected = { -1, -1 };
+    assertEquals(expected[0], ans[0]);
+    assertEquals(expected[1], ans[1]);
+  }
 }

### **Describe your change:**
Added an interface for search algorithms that use matrixes and formatted the classes that implement the interface.

#### References
<!-- Add any reference to previous pull-request or issue -->
Closes #3461

### **Checklist:**

<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Java/blob/master/CONTRIBUTING.md).
- [x] This pull request is all my own work -- I have not plagiarized.
- [x] I know that pull requests will not be merged if they fail the automated tests.
- [x] All filenames are in all uppercase characters with no spaces or dashes.
- [x] All functions and variable names follow Java naming conventions.
- [x] If this pull request resolves one or more open issues then the commit message contains `Fixes: #{$ISSUE_NO}`.

Closes #3461 Add an interface for matrix search algorithms

def parse_java_mvn(log: str) -> dict[str, str]:
    """Parse Maven logs into {identifier: status}."""
    results = {}

    failure_re = re.compile(
        r"^\[ERROR\]\s+(?P<class>[A-Za-z0-9_$.]+)\.(?P<method>[A-Za-z0-9_$.]+?)(?::(?P<line>\d+))?\b"
    )
    running_re = re.compile(r"^(?:\[INFO\]\s+)?Running\s+([A-Za-z0-9_$.]+)$")
    summary_re = re.compile(
        r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)"
    )

    seen_running = set()
    current_running = None

    for raw in log.splitlines():
        line = raw.strip()
        if not line:
            continue
        rm = running_re.match(line)
        if rm:
            running_name = rm.group(1)
            current_running = running_name
            seen_running.add(running_name)
            continue
        fm = failure_re.match(line)
        if fm:
            clazz = fm.group("class")
            method = fm.group("method")
            test_id = f"{clazz}.{method}"
            results[test_id] = "FAILED"
            results.setdefault(clazz, "FAILED")
            continue

        sm = summary_re.search(line)
        if sm:
            _tests_run, failures, errors, skipped = map(int, sm.groups())
            if failures > 0 or errors > 0:
                status = "FAILED"
            elif skipped > 0:
                status = "SKIPPED"
            else:
                status = "PASSED"
            if current_running:
                results.setdefault(current_running, status)
            current_running = None

    if not results and seen_running:
        for clazz in seen_running:
            results.setdefault(clazz, "PASSED")
    return results

bash -c '{ mvn --batch-mode test ; } > /tmp/test.log 2>&1'


diff --git a/main/pom.xml b/main/pom.xml
index 7c94f8575..c96083ed6 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -208,6 +208,11 @@
       <artifactId>jackson-annotations</artifactId>
       <version>${jackson.version}</version>
     </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.datatype</groupId>
+      <artifactId>jackson-datatype-jdk8</artifactId>
+      <version>${jackson.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
diff --git a/main/src/com/google/refine/importers/WikitextImporter.java b/main/src/com/google/refine/importers/WikitextImporter.java
index 87614eba2..09506f8cb 100644
--- a/main/src/com/google/refine/importers/WikitextImporter.java
+++ b/main/src/com/google/refine/importers/WikitextImporter.java
@@ -774,7 +774,6 @@ public class WikitextImporter extends TabularImportingParserBase {
                 "",
                 "entity",
                 true,
-                10,
                 new ArrayList<ColumnDetail>(),
                 1);
         return cfg;
diff --git a/main/src/com/google/refine/model/recon/DataExtensionReconConfig.java b/main/src/com/google/refine/model/recon/DataExtensionReconConfig.java
index f961d0119..da5143b95 100644
--- a/main/src/com/google/refine/model/recon/DataExtensionReconConfig.java
+++ b/main/src/com/google/refine/model/recon/DataExtensionReconConfig.java
@@ -53,7 +53,13 @@ public class DataExtensionReconConfig extends StandardReconConfig {
             String identifierSpace,
             String schemaSpace,
             ReconType type) {
-        super(service, identifierSpace, schemaSpace, type != null ? type.id : null, type != null ? type.name : null, true, 10,
+        super(
+                service,
+                identifierSpace,
+                schemaSpace,
+                type != null ? type.id : null,
+                type != null ? type.name : null,
+                true,
                 new ArrayList<ColumnDetail>());
         this.type = type;
     }
@@ -64,6 +70,11 @@ public class DataExtensionReconConfig extends StandardReconConfig {
         throw new RuntimeException(WARN);
     }

+    @Override
+    public int getBatchSize() {
+        throw new RuntimeException(WARN);
+    }
+
     @Override
     public int getBatchSize(int rowCount) {
         throw new RuntimeException(WARN);
diff --git a/main/src/com/google/refine/model/recon/ReconConfig.java b/main/src/com/google/refine/model/recon/ReconConfig.java
index 62587efee..4f3c049d1 100644
--- a/main/src/com/google/refine/model/recon/ReconConfig.java
+++ b/main/src/com/google/refine/model/recon/ReconConfig.java
@@ -98,6 +98,10 @@ abstract public class ReconConfig {
         return ParsingUtilities.mapper.readValue(json, ReconConfig.class);
     }

+    @Deprecated
+    @JsonIgnore
+    public abstract int getBatchSize();
+
     @JsonIgnore
     public abstract int getBatchSize(int rowCount);

diff --git a/main/src/com/google/refine/model/recon/StandardReconConfig.java b/main/src/com/google/refine/model/recon/StandardReconConfig.java
index 2392310b0..77556fc2b 100644
--- a/main/src/com/google/refine/model/recon/StandardReconConfig.java
+++ b/main/src/com/google/refine/model/recon/StandardReconConfig.java
@@ -42,6 +42,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;

 import com.fasterxml.jackson.annotation.JsonCreator;
@@ -49,6 +50,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -75,6 +78,7 @@ public class StandardReconConfig extends ReconConfig {

     private static final String DEFAULT_SCHEMA_SPACE = "http://localhost/schema";
     private static final String DEFAULT_IDENTIFIER_SPACE = "http://localhost/identifier";
+    private static final int DEFAULT_BATCH_SIZE = 10;

     static public class ColumnDetail {

@@ -88,6 +92,8 @@ public class StandardReconConfig extends ReconConfig {
         /**
          * Unfortunately the format of ColumnDetail is inconsistent in the UI and the backend so we need to support two
          * deserialization formats. See the tests for that.
+         *
+         * FIXME: Clean this up the next time we make breaking changes
          */
         @JsonCreator
         public ColumnDetail(
@@ -143,8 +149,10 @@ public class StandardReconConfig extends ReconConfig {
     final public String typeName;
     @JsonProperty("autoMatch")
     final public boolean autoMatch;
+    @JsonInclude(Include.NON_EMPTY)
+    @JsonSetter(nulls = Nulls.SKIP)
     @JsonProperty("batchSize")
-    final public int batchSize;
+    final public Optional<Integer> batchSize;
     @JsonProperty("columnDetails")
     final public List<ColumnDetail> columnDetails;
     @JsonProperty("limit")
@@ -160,7 +168,7 @@ public class StandardReconConfig extends ReconConfig {
             @JsonProperty("schemaSpace") String schemaSpace,
             @JsonProperty("type") ReconType type,
             @JsonProperty("autoMatch") boolean autoMatch,
-            @JsonProperty("batchSize") int batchSize,
+            @JsonProperty("batchSize") Optional<Integer> batchSize,
             @JsonProperty("columnDetails") List<ColumnDetail> columnDetails,
             @JsonProperty("limit") int limit) {
         this(service, identifierSpace, schemaSpace,
@@ -173,13 +181,66 @@ public class StandardReconConfig extends ReconConfig {
             String service,
             String identifierSpace,
             String schemaSpace,
+            ReconType type,
+            boolean autoMatch,
+            List<ColumnDetail> columnDetails,
+            int limit) {
+        this(service, identifierSpace, schemaSpace,
+                type != null ? type.id : null,
+                type != null ? type.name : null,
+                autoMatch, columnDetails, limit);
+    }

+    /**
+     * Recently added by mistake, so slated for removal quickly after a short deprecation period.
+     *
+     * @since 3.8.0
+     * @deprecated for 3.8.3
+     */
+    @Deprecated(since = "3.8.3")
+    public StandardReconConfig(
+            String service,
+            String identifierSpace,
+            String schemaSpace,
+            ReconType type,
+            boolean autoMatch,
+            int batchSize,
+            List<ColumnDetail> columnDetails,
+            int limit) {
+        this(service, identifierSpace, schemaSpace,
+                type != null ? type.id : null,
+                type != null ? type.name : null,
+                autoMatch, Optional.of(batchSize), columnDetails, limit);
+    }
+
+    public StandardReconConfig(
+            String service,
+            String identifierSpace,
+            String schemaSpace,
+            String typeID,
+            String typeName,
+            boolean autoMatch,
+            List<ColumnDetail> columnDetails) {
+        this(service, identifierSpace, schemaSpace, typeID, typeName, autoMatch, Optional.empty(), columnDetails, 0);
+    }
+
+    /**
+     * Recently added by mistake, so slated for removal quickly after a short deprecation period.
+     *
+     * @since 3.8.0
+     * @deprecated for 3.8.3
+     */
+    @Deprecated(since = "3.8.3")
+    public StandardReconConfig(
+            String service,
+            String identifierSpace,
+            String schemaSpace,
             String typeID,
             String typeName,
             boolean autoMatch,
             int batchSize,
             List<ColumnDetail> columnDetails) {
-        this(service, identifierSpace, schemaSpace, typeID, typeName, autoMatch, batchSize, columnDetails, 0);
+        this(service, identifierSpace, schemaSpace, typeID, typeName, autoMatch, Optional.of(batchSize), columnDetails, 0);
     }

     /**
@@ -200,7 +261,30 @@ public class StandardReconConfig extends ReconConfig {
             String typeID,
             String typeName,
             boolean autoMatch,
-            int batchSize,
+            List<ColumnDetail> columnDetails,
+            int limit) {
+        this(service, identifierSpace, schemaSpace, typeID, typeName, autoMatch, Optional.empty(), columnDetails, limit);
+    }
+
+    /**
+     * @param service
+     * @param identifierSpace
+     * @param schemaSpace
+     * @param typeID
+     * @param typeName
+     * @param autoMatch
+     * @param columnDetails
+     * @param limit
+     *            maximum number of results to return (0 = default)
+     */
+    public StandardReconConfig(
+            String service,
+            String identifierSpace,
+            String schemaSpace,
+            String typeID,
+            String typeName,
+            boolean autoMatch,
+            Optional<Integer> batchSize,
             List<ColumnDetail> columnDetails,
             int limit) {
         this.service = service;
@@ -224,10 +308,20 @@ public class StandardReconConfig extends ReconConfig {
         return null;
     }

+    @Deprecated
+    @Override
+    @JsonIgnore
+    public int getBatchSize() {
+        return DEFAULT_BATCH_SIZE;
+    }
+
     @Override
     @JsonIgnore
     public int getBatchSize(int rowCount) {
-        return Math.min(Math.max(rowCount / 10, 10), batchSize);
+        if (batchSize.isEmpty()) {
+            return DEFAULT_BATCH_SIZE;
+        }
+        return Math.min(Math.max(rowCount / DEFAULT_BATCH_SIZE, DEFAULT_BATCH_SIZE), batchSize.get());
     }

     @Override
@@ -348,6 +442,8 @@ public class StandardReconConfig extends ReconConfig {
         public double score;
         @JsonProperty("match")
         public boolean match = false;
+        // TODO: Do we have a way of testing this for compatibility? Hopefully clients ignore unknown fields
+        @JsonInclude(Include.NON_NULL)
         @JsonProperty("error")
         public ReconCandidate error = null;

@@ -509,6 +605,7 @@ public class StandardReconConfig extends ReconConfig {

         while (recons.size() < jobs.size()) {
             Recon recon = new Recon(historyEntryID, identifierSpace, schemaSpace);
+            // TODO: Needs i18n
             recon.error = "No. of recon objects was less than no. of jobs";
             recon.judgment = Judgment.Error;
             recons.add(recon);
@@ -571,6 +668,7 @@ public class StandardReconConfig extends ReconConfig {
             if (candidate.name != null) {
                 recon.setFeature(Recon.Feature_nameMatch, text.equalsIgnoreCase(candidate.name));
                 recon.setFeature(Recon.Feature_nameLevenshtein,
+                        // TODO: Probably want diacritic folding as well as case folding here for i18n
                         StringUtils.getLevenshteinDistance(StringUtils.lowerCase(text), StringUtils.lowerCase(candidate.name)));
                 recon.setFeature(Recon.Feature_nameWordDistance, wordDistance(text, candidate.name));
             }
@@ -610,7 +708,7 @@ public class StandardReconConfig extends ReconConfig {

     static final protected Set<String> s_stopWords = new HashSet<String>();
     static {
-        // FIXME: This is English specific
+        // FIXME: This is English specific - needs i18n
         s_stopWords.add("the");
         s_stopWords.add("a");
         s_stopWords.add("and");
@@ -622,6 +720,7 @@ public class StandardReconConfig extends ReconConfig {
     }

     static protected Set<String> breakWords(String s) {
+        // TODO: This needs i18n
         String[] words = s.toLowerCase().split("\\s+");

         Set<String> set = new HashSet<String>(words.length);
diff --git a/main/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperation.java b/main/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperation.java
index 0bcf3654c..7240c3779 100644
--- a/main/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperation.java
+++ b/main/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperation.java
@@ -130,7 +130,6 @@ public class ReconMarkNewTopicsOperation extends EngineDependentMassCellOperatio
                         _schemaSpace,
                         null,
                         false,
-                        10,
                         Collections.emptyList(),
                         0);
     }
diff --git a/main/src/com/google/refine/operations/recon/ReconUseValuesAsIdentifiersOperation.java b/main/src/com/google/refine/operations/recon/ReconUseValuesAsIdentifiersOperation.java
index 41dbcbc4a..d3df56919 100644
--- a/main/src/com/google/refine/operations/recon/ReconUseValuesAsIdentifiersOperation.java
+++ b/main/src/com/google/refine/operations/recon/ReconUseValuesAsIdentifiersOperation.java
@@ -74,7 +74,7 @@ public class ReconUseValuesAsIdentifiersOperation extends EngineDependentMassCel
         this.service = service;
         this.identifierSpace = identifierSpace;
         this.schemaSpace = schemaSpace;
-        this.reconConfig = new StandardReconConfig(service, identifierSpace, schemaSpace, null, null, true, 10, Collections.emptyList());
+        this.reconConfig = new StandardReconConfig(service, identifierSpace, schemaSpace, null, null, true, Collections.emptyList());
     }

     @Override
diff --git a/main/src/com/google/refine/util/ParsingUtilities.java b/main/src/com/google/refine/util/ParsingUtilities.java
index f7bfbd392..806ce1a4c 100644
--- a/main/src/com/google/refine/util/ParsingUtilities.java
+++ b/main/src/com/google/refine/util/ParsingUtilities.java
@@ -66,6 +66,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.ser.FilterProvider;
 import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import org.apache.commons.codec.DecoderException;
 import org.apache.commons.codec.net.URLCodec;
 import org.apache.commons.io.IOUtils;
@@ -90,6 +91,7 @@ public class ParsingUtilities {
         module.addDeserializer(Instant.class, new SerializationFilters.InstantDeserializer());

         mapper.registerModule(module);
+        mapper.registerModule(new Jdk8Module()/* .configureAbsentsAsNulls(true) */);
         mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
         mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
     }
diff --git a/main/webapp/modules/core/about.html b/main/webapp/modules/core/about.html
index 50b1cf423..0af5a5a37 100644
--- a/main/webapp/modules/core/about.html
+++ b/main/webapp/modules/core/about.html
@@ -62,7 +62,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-
+

OpenRefine

diff --git a/main/webapp/modules/core/error.vt b/main/webapp/modules/core/error.vt
index effae65d6..6c997645a 100644
--- a/main/webapp/modules/core/error.vt
+++ b/main/webapp/modules/core/error.vt
@@ -43,7 +43,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-
+

OpenRefine

diff --git a/main/webapp/modules/core/images/logo-gem-126.svg b/main/webapp/modules/core/images/logo-gem-126.svg
new file mode 100644
index 000000000..ae944f61d
--- /dev/null
+++ b/main/webapp/modules/core/images/logo-gem-126.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.1" viewBox="-20 -6.5 160 151" xmlns="http://www.w3.org/2000/svg">
+ <g transform="translate(0 -8.2001)" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width="4px">
+  <polygon points="31.5 31.5 63 31.5 31.5 63" fill="#00dcff"/>
+  <polygon points="31.5 31.5 0 63 31.5 63" fill="#0ff"/>
+  <polygon points="94.5 31.5 63 31.5 94.5 63" fill="#00b4ff"/>
+  <polygon points="63 31.5 94.5 63 31.5 63" fill="#00c8ff"/>
+  <polygon points="94.5 31.5 126 63 94.5 63" fill="#00a0ff"/>
+  <polygon points="0 63 63 126 31.5 63" fill="#00c8ff"/>
+  <polygon points="31.5 63 63 126 94.5 63" fill="#00a0ff"/>
+  <polygon points="94.5 63 63 126 126 63" fill="#0078ff"/>
+ </g>
+</svg>
diff --git a/main/webapp/modules/core/index.vt b/main/webapp/modules/core/index.vt
index 327ee612a..ecac2668a 100644
--- a/main/webapp/modules/core/index.vt
+++ b/main/webapp/modules/core/index.vt
@@ -44,7 +44,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-
+

OpenRefine

@@ -55,7 +55,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-
+

diff --git a/main/webapp/modules/core/preferences.vt b/main/webapp/modules/core/preferences.vt
index e1b7d5372..4bbb16547 100644
--- a/main/webapp/modules/core/preferences.vt
+++ b/main/webapp/modules/core/preferences.vt
@@ -44,7 +44,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-
+

OpenRefine

diff --git a/main/webapp/modules/core/project.vt b/main/webapp/modules/core/project.vt
index 7c21ea12a..8d4fe782e 100644
--- a/main/webapp/modules/core/project.vt
+++ b/main/webapp/modules/core/project.vt
@@ -49,7 +49,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-
+

OpenRefine

diff --git a/main/webapp/modules/core/styles/project/sidebar.css b/main/webapp/modules/core/styles/project/sidebar.css
index 65b5afa9b..b3274079f 100644
--- a/main/webapp/modules/core/styles/project/sidebar.css
+++ b/main/webapp/modules/core/styles/project/sidebar.css
@@ -91,7 +91,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   padding: 20px 0 0 10px;
   font-size: 1.15em;
   font-weight: bold;
-  background: url(../../images/logo-gem-126.png) no-repeat top right;
+  background: url(../../images/logo-gem-126.svg) no-repeat top right;
   background-size: 43px;
 }

diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/NewEntityLibraryTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/NewEntityLibraryTest.java
index d3c297c60..b25f029fe 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/NewEntityLibraryTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/NewEntityLibraryTest.java
@@ -62,7 +62,7 @@ public class NewEntityLibraryTest extends WikidataRefineTest {
         Project project = createProject(TestingData.inceptionColumns,
                 TestingData.inceptionProjectGridWithNewItem);
         StandardReconConfig config = new StandardReconConfig("http://my.endpoint",
-                "http://my.schema", "http://my.schema", "Q5", "human", true, 10, Collections.emptyList());
+                "http://my.schema", "http://my.schema", "Q5", "human", true, Collections.emptyList());
         project.columnModel.columns.get(0).setReconConfig(config);

         project.rows.get(0).cells.set(0, TestingData.makeNewItemCell(3289L, "University of Ljubljana"));
diff --git a/main/tests/server/src/com/google/refine/commands/recon/ReconJudgeOneCellCommandTest.java b/main/tests/server/src/com/google/refine/commands/recon/ReconJudgeOneCellCommandTest.java
index 6e334149e..8425a4549 100644
--- a/main/tests/server/src/com/google/refine/commands/recon/ReconJudgeOneCellCommandTest.java
+++ b/main/tests/server/src/com/google/refine/commands/recon/ReconJudgeOneCellCommandTest.java
@@ -76,7 +76,7 @@ public class ReconJudgeOneCellCommandTest extends RefineTest {
                 "http://my.recon.service/rdf/schema",
                 "type3894",
                 "octopus",
-                true, 10,
+                true,
                 Collections.emptyList(),
                 5);
         reconciled.setReconConfig(config);
diff --git a/main/tests/server/src/com/google/refine/model/ColumnTests.java b/main/tests/server/src/com/google/refine/model/ColumnTests.java
index f367dd415..da6c02902 100644
--- a/main/tests/server/src/com/google/refine/model/ColumnTests.java
+++ b/main/tests/server/src/com/google/refine/model/ColumnTests.java
@@ -54,7 +54,6 @@ public class ColumnTests {
                 + "   \"schemaSpace\":\"http://www.wikidata.org/prop/direct/\","
                 + "   \"type\":{\"id\":\"Q43229\",\"name\":\"organization\"},"
                 + "   \"autoMatch\":true,"
-                + "   \"batchSize\":10,"
                 + "   \"columnDetails\":["
                 + "      {\"column\":\"organization_country\",\"propertyName\":\"SPARQL: P17/P297\",\"propertyID\":\"P17/P297\"},"
                 + "      {\"column\":\"organization_id\",\"propertyName\":\"SPARQL: P3500|P2427\",\"propertyID\":\"P3500|P2427\"}"
diff --git a/main/tests/server/src/com/google/refine/model/recon/StandardReconConfigTests.java b/main/tests/server/src/com/google/refine/model/recon/StandardReconConfigTests.java
index 005b92f34..0a69a985e 100644
--- a/main/tests/server/src/com/google/refine/model/recon/StandardReconConfigTests.java
+++ b/main/tests/server/src/com/google/refine/model/recon/StandardReconConfigTests.java
@@ -77,7 +77,7 @@ public class StandardReconConfigTests extends RefineTest {
     private class StandardReconConfigStub extends StandardReconConfig {

         public StandardReconConfigStub() {
-            super("", "", "", "", "", false, 10, new ArrayList<ColumnDetail>());
+            super("", "", "", "", "", false, new ArrayList<ColumnDetail>());
         }

         public double wordDistanceTest(String s1, String s2) {
@@ -118,7 +118,41 @@ public class StandardReconConfigTests extends RefineTest {
                 "                \"name\": \"scientific article\"\n" +
                 "        },\n" +
                 "        \"autoMatch\": true,\n" +
-                "        \"batchSize\": 10,\n" +
+                "        \"columnDetails\": [\n" +
+                "           {\n" +
+                "             \"column\": \"organization_country\",\n" +
+                "             \"propertyName\": \"SPARQL: P17/P297\",\n" +
+                "             \"propertyID\": \"P17/P297\"\n" +
+                "           },\n" +
+                "           {\n" +
+                "             \"column\": \"organization_id\",\n" +
+                "             \"propertyName\": \"SPARQL: P3500|P2427\",\n" +
+                "             \"propertyID\": \"P3500|P2427\"\n" +
+                "           }\n" +
+                "        ],\n" +
+                "        \"limit\": 0\n" +
+                " }";
+        ReconConfig config = ReconConfig.reconstruct(json);
+        TestUtils.isSerializedTo(config, json);
+
+        // the "mode" only appears once in the serialization result
+        String fullJson = ParsingUtilities.mapper.writeValueAsString(config);
+        assertEquals(fullJson.indexOf("\"mode\"", fullJson.indexOf("\"mode\"") + 1), -1);
+    }
+
+    @Test
+    public void serializeStandardReconConfigWithBatchSize() throws Exception {
+        String json = " {\n" +
+                "        \"mode\": \"standard-service\",\n" +
+                "        \"service\": \"https://tools.wmflabs.org/openrefine-wikidata/en/api\",\n" +
+                "        \"identifierSpace\": \"http://www.wikidata.org/entity/\",\n" +
+                "        \"schemaSpace\": \"http://www.wikidata.org/prop/direct/\",\n" +
+                "        \"type\": {\n" +
+                "                \"id\": \"Q13442814\",\n" +
+                "                \"name\": \"scientific article\"\n" +
+                "        },\n" +
+                "        \"autoMatch\": true,\n" +
+                "        \"batchSize\": 20,\n" +
                 "        \"columnDetails\": [\n" +
                 "           {\n" +
                 "             \"column\": \"organization_country\",\n" +
diff --git a/main/tests/server/src/com/google/refine/operations/recon/ReconJudgeSimilarCellsOperationTests.java b/main/tests/server/src/com/google/refine/operations/recon/ReconJudgeSimilarCellsOperationTests.java
index cf18dc1f5..de60cb80d 100644
--- a/main/tests/server/src/com/google/refine/operations/recon/ReconJudgeSimilarCellsOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/recon/ReconJudgeSimilarCellsOperationTests.java
@@ -78,7 +78,6 @@ public class ReconJudgeSimilarCellsOperationTests extends RefineTest {
                 schemaSpace,
                 null,
                 true,
-                10,
                 Collections.emptyList(),
                 5);
         project = createProject(
@@ -134,7 +133,8 @@ public class ReconJudgeSimilarCellsOperationTests extends RefineTest {
                 "http://my.database/schema/",
                 null,
                 null,
-                true, 10, Collections.emptyList());
+                true,
+                Collections.emptyList());
         column.setReconConfig(config);

         AbstractOperation op = new ReconJudgeSimilarCellsOperation(
diff --git a/main/tests/server/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperationTests.java b/main/tests/server/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperationTests.java
index dd71428d6..1e4759d17 100644
--- a/main/tests/server/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/recon/ReconMarkNewTopicsOperationTests.java
@@ -113,7 +113,7 @@ public class ReconMarkNewTopicsOperationTests extends RefineTest {
                 "http://foo.com/identifierSpace",
                 "http://foo.com/schemaSpace",
                 null,
-                false, 10,
+                false,
                 Collections.emptyList(),
                 0);

diff --git a/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java b/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java
index 87fb12401..b03b83e35 100644
--- a/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java
+++ b/main/tests/server/src/com/google/refine/operations/recon/ReconOperationTests.java
@@ -87,7 +87,6 @@ public class ReconOperationTests extends RefineTest {
             + "   \"schemaSpace\":\"http://www.wikidata.org/prop/direct/\","
             + "   \"type\":{\"id\":\"Q5\",\"name\":\"human\"},"
             + "   \"autoMatch\":true,"
-            + "   \"batchSize\":10,"
             + "   \"columnDetails\":[],"
             + "   \"limit\":0"
             + "},"
@@ -174,6 +173,7 @@ public class ReconOperationTests extends RefineTest {
         recon3.judgment = Judgment.Matched;

         reconConfig = mock(StandardReconConfig.class, withSettings().serializable());
+        doReturn(2).when(reconConfig).getBatchSize();
         doReturn(2).when(reconConfig).getBatchSize(anyInt());
         // mock identifierSpace, service and schemaSpace
         when(reconConfig.batchRecon(eq(Arrays.asList(job1, job2)), anyLong())).thenReturn(Arrays.asList(recon1, recon2));
@@ -236,6 +236,7 @@ public class ReconOperationTests extends RefineTest {
         List<Recon> reconList = Arrays.asList((Recon) null, (Recon) null, (Recon) null);
         ReconJob reconJob = mock(ReconJob.class);
         when(reconConfig.batchRecon(Mockito.any(), Mockito.anyLong())).thenReturn(reconList);
+        when(reconConfig.getBatchSize()).thenReturn(10);
         when(reconConfig.getBatchSize(project.rows.size())).thenReturn(10);
         when(reconConfig.createJob(Mockito.eq(project), Mockito.anyInt(), Mockito.any(), Mockito.any(), Mockito.any()))
                 .thenReturn(reconJob);

Draft while I figure out the intricacies of Jackson annotations...

Fixes #6652

Changes proposed in this pull request:
- Restore deleted public constructors & getters

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/main/src/com/google/refine/importers/ImportingParserBase.java b/main/src/com/google/refine/importers/ImportingParserBase.java
index 06b09ff37..50eaf78fa 100644
--- a/main/src/com/google/refine/importers/ImportingParserBase.java
+++ b/main/src/com/google/refine/importers/ImportingParserBase.java
@@ -117,6 +117,7 @@ abstract public class ImportingParserBase implements ImportingParser {
             final MultiFileReadingProgress progress) throws IOException {
         final File file = ImportingUtilities.getFile(job, fileRecord);
         final String fileSource = ImportingUtilities.getFileSource(fileRecord);
+        final String fileName = ImportingUtilities.getFileName(fileRecord);
         final String archiveFileName = ImportingUtilities.getArchiveFileName(fileRecord);
         int filenameColumnIndex = -1;
         int archiveColumnIndex = -1;
@@ -136,7 +137,7 @@ abstract public class ImportingParserBase implements ImportingParser {
                 }

                 if (useInputStream) {
-                    parseOneFile(project, metadata, job, fileSource, inputStream, limit, options, exceptions);
+                    parseOneFile(project, metadata, job, fileName, inputStream, limit, options, exceptions);
                 } else {
                     // Although this is called "common" encoding, it may represent the user's override of the encoding
                     String commonEncoding = JSONUtilities.getString(options, "encoding", null);
@@ -147,7 +148,7 @@ abstract public class ImportingParserBase implements ImportingParser {
                     Reader reader = ImportingUtilities.getReaderFromStream(
                             inputStream, fileRecord, commonEncoding);

-                    parseOneFile(project, metadata, job, fileSource, reader, limit, options, exceptions);
+                    parseOneFile(project, metadata, job, fileName, reader, limit, options, exceptions);
                 }

                 // Fill in filename and archive name column for all rows added from this file
diff --git a/main/src/com/google/refine/importing/ImportingUtilities.java b/main/src/com/google/refine/importing/ImportingUtilities.java
index 97882320d..6209f9a7b 100644
--- a/main/src/com/google/refine/importing/ImportingUtilities.java
+++ b/main/src/com/google/refine/importing/ImportingUtilities.java
@@ -597,6 +597,13 @@ public class ImportingUtilities {
                 JSONUtilities.getString(fileRecord, "fileName", "unknown"));
     }

+    static public String getFileName(ObjectNode fileRecord) {
+        return JSONUtilities.getString(
+                fileRecord,
+                "fileName",
+                "unknown");
+    }
+
     static public String getArchiveFileName(ObjectNode fileRecord) {
         return JSONUtilities.getString(
                 fileRecord,
diff --git a/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js b/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js
index 8735bbb14..87b210369 100644
--- a/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js
+++ b/main/webapp/modules/core/scripts/views/data-table/cell-renderers/recon-renderer.js
@@ -122,7 +122,7 @@ class ReconCellRenderer {
               a.attr("href", encodeURI(service.view.url.replace("{{id}}", candidate.id)));
             }

-            self.previewOnHover(service, candidate, liSpan.parent(), liSpan, true, rowIndex, cellIndex, cell);
+            self.previewOnHover(service, candidate, liSpan.parent(), liSpan, true, rowIndex, cellIndex, cell, cellUI);

             var score;
             if (candidate.score < 1) {
@@ -411,7 +411,7 @@ class ReconCellRenderer {
       command,
       params,
       bodyParams,
-      { cellsChanged: true, columnStatsChanged: columnStatsChanged },
+      { columnStatsChanged: columnStatsChanged },
       {
         onDone: function(o) {
           if (o.cell.r) {
diff --git a/pom.xml b/pom.xml
index b102e5af8..677a8d97d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -118,7 +118,7 @@
     <maven-dependency-plugin.version>3.7.1</maven-dependency-plugin.version>
     <jacoco-maven-plugin.version>0.8.12</jacoco-maven-plugin.version>
     <surefire.version>3.3.0</surefire.version>
-    <maven-jar-plugin.version>3.4.1</maven-jar-plugin.version>
+    <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
     <maven-clean-plugin.version>3.4.0</maven-clean-plugin.version>
     <git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
     <maven-deploy-plugin.version>3.1.2</maven-deploy-plugin.version>

diff --git a/main/tests/cypress/cypress/e2e/project/grid/column/reconcile/actions/cell-buttons.cy.js b/main/tests/cypress/cypress/e2e/project/grid/column/reconcile/actions/cell-buttons.cy.js
index 1ac0b9e1d..7ee8cfef5 100644
--- a/main/tests/cypress/cypress/e2e/project/grid/column/reconcile/actions/cell-buttons.cy.js
+++ b/main/tests/cypress/cypress/e2e/project/grid/column/reconcile/actions/cell-buttons.cy.js
@@ -51,6 +51,30 @@ describe('In-cell reconciliation buttons', () => {
         //confirming the no. of candidates after we click on see less
         cy.getCell(0, 'species').find('.data-table-recon-candidate:visible').should('have.length', 4);
     });
+
+    it('Matching a single cell preserves pagination', () => {
+        cy.visitOpenRefine();
+        cy.navigateTo('Import project');
+        cy.get('.grid-layout').should('to.contain', 'Locate an existing Refine project file');
+        cy.get('#project-tar-file-input').attachFile('reconciled-project-no-automatch.zip')
+        cy.get('#import-project-button').click();
+
+        // Set a smaller page number to make sure there are multiple pages
+        cy.get('.viewPanel-pagingControls-page:nth-of-type(1)').click();
+        // Go to the next page
+        cy.get('.viewpanel-paging .action').first().click();
+        // Check that we have a single row in view (second page)
+        cy.get('.data-table tbody tr').should('have.length', 1);
+
+        // Match the first recon candidate in the cell
+        cy.get('.data-table-recon-match').first().click();
+
+        // Make sure the cell is matched (no recon candidates are visible anymore)
+        cy.get('.data-table-recon-match').should('not.exist');
+        // We are still in the second page
+        cy.get('.data-table tbody tr').should('have.length', 1);
+    });
+
 });

diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index d7f898e78..acbaf6cf8 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -20,7 +20,7 @@
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
-        "eslint": "^9.5.0",
+        "eslint": "^9.6.0",
         "eslint-config-google": "^0.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-cypress": "^3.3.0",
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index c494915dd..07014a9b7 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -51,14 +51,14 @@
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
   integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==

-"@eslint/config-array@^0.16.0":
-  version "0.16.0"
-  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.16.0.tgz#bb3364fc39ee84ec3a62abdc4b8d988d99dfd706"
-  integrity sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==
+"@eslint/config-array@^0.17.0":
+  version "0.17.0"
+  resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.17.0.tgz#ff305e1ee618a00e6e5d0485454c8d92d94a860d"
+  integrity sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==
   dependencies:
     "@eslint/object-schema" "^2.1.4"
     debug "^4.3.1"
-    minimatch "^3.0.5"
+    minimatch "^3.1.2"

 "@eslint/eslintrc@^3.1.0":
   version "3.1.0"
@@ -75,10 +75,10 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"

-"@eslint/js@9.5.0":
-  version "9.5.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.5.0.tgz#0e9c24a670b8a5c86bff97b40be13d8d8f238045"
-  integrity sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==
+"@eslint/js@9.6.0":
+  version "9.6.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.6.0.tgz#5b0cb058cc13d9c92d4e561d3538807fa5127c95"
+  integrity sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==

 "@eslint/object-schema@^2.1.4":
   version "2.1.4"
@@ -148,10 +148,10 @@ acorn-jsx@^5.3.2:
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==

-acorn@^8.11.3:
-  version "8.11.3"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
-  integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
+acorn@^8.12.0:
+  version "8.12.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c"
+  integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==

 aggregate-error@^3.0.0:
   version "3.1.0"
@@ -592,16 +592,16 @@ eslint-visitor-keys@^4.0.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
   integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==

-eslint@^9.5.0:
-  version "9.5.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.5.0.tgz#11856034b94a9e1a02cfcc7e96a9f0956963cd2f"
-  integrity sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==
+eslint@^9.6.0:
+  version "9.6.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.6.0.tgz#9f54373afa15e1ba356656a8d96233182027fb49"
+  integrity sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.6.1"
-    "@eslint/config-array" "^0.16.0"
+    "@eslint/config-array" "^0.17.0"
     "@eslint/eslintrc" "^3.1.0"
-    "@eslint/js" "9.5.0"
+    "@eslint/js" "9.6.0"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@humanwhocodes/retry" "^0.3.0"
     "@nodelib/fs.walk" "^1.2.8"
@@ -612,7 +612,7 @@ eslint@^9.5.0:
     escape-string-regexp "^4.0.0"
     eslint-scope "^8.0.1"
     eslint-visitor-keys "^4.0.0"
-    espree "^10.0.1"
+    espree "^10.1.0"
     esquery "^1.5.0"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
@@ -632,12 +632,12 @@ eslint@^9.5.0:
     strip-ansi "^6.0.1"
     text-table "^0.2.0"

-espree@^10.0.1:
-  version "10.0.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f"
-  integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==
+espree@^10.0.1, espree@^10.1.0:
+  version "10.1.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56"
+  integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==
   dependencies:
-    acorn "^8.11.3"
+    acorn "^8.12.0"
     acorn-jsx "^5.3.2"
     eslint-visitor-keys "^4.0.0"

@@ -1186,7 +1186,7 @@ mimic-fn@^2.1.0:
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz"
   integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==

-minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2:
+minimatch@^3.0.4, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
   integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
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 8e6851236..faca5a4a3 100644
--- a/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
+++ b/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
@@ -41,6 +41,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URISyntaxException;
+import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -564,4 +565,14 @@ public class ImportingUtilitiesTests extends ImporterTest {
             }
         }
     }
+
+    @Test
+    public void testGetFileName() {
+        ObjectNode fileRecord = ParsingUtilities.mapper.createObjectNode();
+        String fileName = "aFileName";
+
+        JSONUtilities.safePut(fileRecord, "fileName", fileName);
+
+        assertEquals(fileName, ImportingUtilities.getFileName(fileRecord));
+    }
 }

Fixes #6695.

Changes proposed in this pull request:
- revert #6239 which forces the re-fetching and re-rendering of the entire grid
- add missing `cellUI` parameter to enable in-place rendering of the updated cell after a single match action
- add a Cypress regression test

I would like to backport this to 3.8.

Fix preservation of pagination when matching a single cell


diff --git a/main/src/com/google/refine/importers/TextFormatGuesser.java b/main/src/com/google/refine/importers/TextFormatGuesser.java
index ca9c6f96b..b9a08a674 100644
--- a/main/src/com/google/refine/importers/TextFormatGuesser.java
+++ b/main/src/com/google/refine/importers/TextFormatGuesser.java
@@ -34,13 +34,13 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
-import java.util.Arrays;

 import org.apache.commons.io.input.BoundedInputStream;
 import org.apache.commons.lang.StringUtils;

 import com.google.common.base.CharMatcher;
 import com.google.refine.importing.FormatGuesser;
+import com.google.refine.importing.ImportingUtilities;

 public class TextFormatGuesser implements FormatGuesser {

@@ -51,7 +51,7 @@ public class TextFormatGuesser implements FormatGuesser {
     @Override
     public String guess(File file, String encoding, String seedFormat) {
         try (InputStream fis = new FileInputStream(file)) {
-            if (isCompressed(file)) {
+            if (ImportingUtilities.isCompressed(file)) {
                 return "binary";
             }
             ;
@@ -132,19 +132,4 @@ public class TextFormatGuesser implements FormatGuesser {
         return null;
     }

-    private boolean isCompressed(File file) throws IOException {
-        // Check for common compressed file types to protect ourselves from binary data
-        try (InputStream is = new FileInputStream(file)) {
-            byte[] magic = new byte[4];
-            int count = is.read(magic);
-            if (count == 4 && Arrays.equals(magic, new byte[] { 0x50, 0x4B, 0x03, 0x04 }) || // zip
-                    Arrays.equals(magic, new byte[] { 0x50, 0x4B, 0x07, 0x08 }) ||
-                    (magic[0] == 0x1F && magic[1] == (byte) 0x8B) // gzip
-            ) {
-                return true;
-            }
-        }
-        return false;
-    }
-
 }
diff --git a/main/src/com/google/refine/importing/EncodingGuesser.java b/main/src/com/google/refine/importing/EncodingGuesser.java
index 605f93c47..797c355f1 100644
--- a/main/src/com/google/refine/importing/EncodingGuesser.java
+++ b/main/src/com/google/refine/importing/EncodingGuesser.java
@@ -1,31 +1,32 @@

 package com.google.refine.importing;

-import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
 import java.util.List;

-import org.apache.commons.lang.StringUtils;
-import org.mozilla.universalchardet.UniversalDetector;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.commons.lang.StringUtils;
+import org.mozilla.universalchardet.UnicodeBOMInputStream;
+import org.mozilla.universalchardet.UniversalDetector;

 import com.google.refine.util.JSONUtilities;

 /**
- * This class tries to find the correct encoding based on the http://site.icu-project.org/ and the icu4j library
- * http://site.icu-project.org/home/why-use-icu4j.
+ * This class tries to find the correct encoding based on https://github.com/albfernandez/juniversalchardet which is a
+ * Java port of Mozilla's universalchardet library
+ * https://hg.mozilla.org/mozilla-central/file/tip/extensions/universalchardet/
  *
  * @author Steffen Stundzig
  */
 public final class EncodingGuesser {

-    public final static void guess(final ImportingJob job)
-            throws FileNotFoundException, IOException {
+    public static final String UTF_8_BOM = "UTF-8-BOM";
+
+    public static void guess(final ImportingJob job)
+            throws IOException {
         ObjectNode retrievalRecord = job.getRetrievalRecord();
         if (retrievalRecord != null) {
             ArrayNode fileRecords = JSONUtilities.getArray(retrievalRecord, "files");
@@ -38,9 +39,13 @@ public final class EncodingGuesser {
                     if (StringUtils.isBlank(encoding)) {
                         String location = JSONUtilities.getString(record, "location", null);
                         if (location != null) {
-                            try (InputStream is = new BufferedInputStream(
+                            try (UnicodeBOMInputStream is = new UnicodeBOMInputStream(
                                     new FileInputStream(new File(job.getRawDataDir(), location)))) {
                                 String detected = UniversalDetector.detectCharset(is);
+                                UnicodeBOMInputStream.BOM bom = is.getBOM();
+                                if (UnicodeBOMInputStream.BOM.UTF_8.equals(bom)) {
+                                    detected = UTF_8_BOM;
+                                }
                                 if (detected != null) {
                                     JSONUtilities.safePut(record, "encoding", detected);
                                 }
diff --git a/main/src/com/google/refine/importing/ImportingUtilities.java b/main/src/com/google/refine/importing/ImportingUtilities.java
index 27ff6a5d8..5a2f2d923 100644
--- a/main/src/com/google/refine/importing/ImportingUtilities.java
+++ b/main/src/com/google/refine/importing/ImportingUtilities.java
@@ -33,6 +33,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 package com.google.refine.importing;

+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -69,7 +71,6 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
-import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
 import org.apache.commons.fileupload.FileItem;
 import org.apache.commons.fileupload.FileUploadException;
 import org.apache.commons.fileupload.ProgressListener;
@@ -84,6 +85,7 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.mozilla.universalchardet.UnicodeBOMInputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;

@@ -349,7 +351,6 @@ public class ImportingUtilities {
                         if (httpClient.getResponse(urlString, null, responseHandler) != null) {
                             archiveCount++;
                         }
-                        ;
                         downloadCount++;
                     } else {
                         // Fallback handling for non HTTP connections (only FTP?)
@@ -435,19 +436,19 @@ public class ImportingUtilities {

         update.totalExpectedSize += length;

-        progress.setProgress("Downloading " + url.toString(),
+        progress.setProgress("Downloading " + url, // TODO: Localize
                 calculateProgressPercent(update.totalExpectedSize, update.totalRetrievedSize));

         long actualLength = saveStreamToFile(stream, file, update);
         JSONUtilities.safePut(fileRecord, "size", actualLength);
         if (actualLength == 0) {
-            throw new IOException("No content found in " + url.toString());
+            throw new IOException("No content found in " + url);
         } else if (length >= 0) {
             update.totalExpectedSize += (actualLength - length);
         } else {
             update.totalExpectedSize += actualLength;
         }
-        progress.setProgress("Saving " + url.toString() + " locally",
+        progress.setProgress("Saving " + url + " locally", // TODO: Localize
                 calculateProgressPercent(update.totalExpectedSize, update.totalRetrievedSize));
         return postProcessRetrievedFile(rawDataDir, file, fileRecord, fileRecords, progress);
     }
@@ -528,11 +529,23 @@ public class ImportingUtilities {
             encoding = commonEncoding;
         }
         if (encoding != null) {
-            try {
-                return new InputStreamReader(inputStream, encoding);
-            } catch (UnsupportedEncodingException e) {
-                // Ignore and fall through
+
+            // Special case for UTF-8 with BOM
+            if (EncodingGuesser.UTF_8_BOM.equals(encoding)) {
+                try {
+                    return new InputStreamReader(new UnicodeBOMInputStream(inputStream, true), UTF_8);
+                } catch (IOException e) {
+                    throw new RuntimeException("Exception from UnicodeBOMInputStream", e);
+                }
+            } else {
+                try {
+                    return new InputStreamReader(inputStream, encoding);
+                } catch (UnsupportedEncodingException e) {
+                    // This should never happen since they picked from a list of supported encodings
+                    throw new RuntimeException("Unsupported encoding: " + encoding, e);
+                }
             }
+
         }
         return new InputStreamReader(inputStream);
     }
@@ -677,7 +690,7 @@ public class ImportingUtilities {
                     || "application/x-zip-compressed".equals(contentType)
                     || "application/zip".equals(contentType)
                     || "application/x-compressed".equals(contentType)
-                    || "multipar/x-zip".equals(contentType)) {
+                    || "multipart/x-zip".equals(contentType)) {
                 return new ZipInputStream(new FileInputStream(file));
             } else if (fileName.endsWith(".kmz")) {
                 return new ZipInputStream(new FileInputStream(file));
@@ -689,13 +702,28 @@ public class ImportingUtilities {

     private static boolean isFileGZipped(File file) {
         int magic = 0;
-        try (RandomAccessFile raf = new RandomAccessFile(file, "r");) {
+        try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
             magic = raf.read() & 0xff | ((raf.read() << 8) & 0xff00);
         } catch (IOException ignored) {
         }
         return magic == GZIPInputStream.GZIP_MAGIC;
     }

+    public static boolean isCompressed(File file) throws IOException {
+        // Check for common compressed file types to protect ourselves from binary data
+        try (InputStream is = new FileInputStream(file)) {
+            byte[] magic = new byte[4];
+            int count = is.read(magic);
+            if (count == 4 && Arrays.equals(magic, new byte[] { 0x50, 0x4B, 0x03, 0x04 }) || // zip
+                    Arrays.equals(magic, new byte[] { 0x50, 0x4B, 0x07, 0x08 }) ||
+                    (magic[0] == 0x1F && magic[1] == (byte) 0x8B) // gzip
+            ) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     // FIXME: This is wasteful of space and time. We should try to process on the fly
     static private boolean explodeArchive(
             File rawDataDir,
@@ -888,7 +916,7 @@ public class ImportingUtilities {
                 }
             }

-            // If nothing matches the best format but we have some files,
+            // If nothing matches the best format, but we have some files,
             // then select them all
             if (fileSelectionIndexes.size() == 0 && count > 0) {
                 for (int i = 0; i < count; i++) {
diff --git a/main/webapp/package-lock.json b/main/webapp/package-lock.json
index 824f48858..75ca6ca17 100644
--- a/main/webapp/package-lock.json
+++ b/main/webapp/package-lock.json
@@ -9,7 +9,7 @@
       "dependencies": {
         "@wikimedia/jquery.i18n": "1.0.9",
         "datejs-coolite": "1.0.0",
-        "jquery": "3.7.0",
+        "jquery": "3.7.1",
         "jquery-migrate": "3.4.1",
         "js-cookie": "3.0.5",
         "moment": "2.29.4",
@@ -35,9 +35,9 @@
       "integrity": "sha512-kppYUwn9R2pzsoChLsDlWLtKTwjf5k9Z3fx8CNKjwvhYfpadKIFoL+16dDJvftiXKwT+ixh2PRslZsfzp+1ZVA=="
     },
     "node_modules/jquery": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
-      "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
     },
     "node_modules/jquery-migrate": {
       "version": "3.4.1",
@@ -94,9 +94,9 @@
       "integrity": "sha512-kppYUwn9R2pzsoChLsDlWLtKTwjf5k9Z3fx8CNKjwvhYfpadKIFoL+16dDJvftiXKwT+ixh2PRslZsfzp+1ZVA=="
     },
     "jquery": {
-      "version": "3.7.0",
-      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
-      "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
     },
     "jquery-migrate": {
       "version": "3.4.1",
diff --git a/main/webapp/package.json b/main/webapp/package.json
index 52f8ccdc4..44211bc1b 100644
--- a/main/webapp/package.json
+++ b/main/webapp/package.json
@@ -12,7 +12,7 @@
   "dependencies": {
     "@wikimedia/jquery.i18n": "1.0.9",
     "datejs-coolite": "1.0.0",
-    "jquery": "3.7.0",
+    "jquery": "3.7.1",
     "jquery-migrate": "3.4.1",
     "js-cookie": "3.0.5",
     "moment": "2.29.4",

diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index bb2544479..9e190c38d 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -20,10 +20,10 @@
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
-        "eslint": "^8.47.0",
+        "eslint": "^8.48.0",
         "eslint-config-google": "^0.14.0",
         "eslint-config-prettier": "^9.0.0",
         "eslint-plugin-cypress": "^2.14.0",
-        "prettier": "3.0.2"
+        "prettier": "3.0.3"
     }
 }
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 74b9f5efc..116e2bbbf 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -66,10 +66,10 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"

-"@eslint/js@^8.47.0":
-  version "8.47.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d"
-  integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==
+"@eslint/js@8.48.0":
+  version "8.48.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
+  integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==

 "@humanwhocodes/config-array@^0.11.10":
   version "0.11.10"
@@ -590,15 +590,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==

-eslint@^8.47.0:
-  version "8.47.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806"
-  integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==
+eslint@^8.48.0:
+  version "8.48.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155"
+  integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.6.1"
     "@eslint/eslintrc" "^2.1.2"
-    "@eslint/js" "^8.47.0"
+    "@eslint/js" "8.48.0"
     "@humanwhocodes/config-array" "^0.11.10"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
@@ -1303,10 +1303,10 @@ prelude-ls@^1.2.1:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==

-prettier@3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
-  integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
+prettier@3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
+  integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==

 pretty-bytes@^5.6.0:
   version "5.6.0"
diff --git a/main/tests/data/csv-with-bom.csv b/main/tests/data/csv-with-bom.csv
new file mode 100644
index 000000000..bdb3bcc64
--- /dev/null
+++ b/main/tests/data/csv-with-bom.csv
@@ -0,0 +1,3 @@
+ColA,ColB
+A1,B1
+A2,B2
\ No newline at end of file
diff --git a/main/tests/server/src/com/google/refine/importing/EncodingGuesserTests.java b/main/tests/server/src/com/google/refine/importing/EncodingGuesserTests.java
index 73d60f011..d5e1a060a 100644
--- a/main/tests/server/src/com/google/refine/importing/EncodingGuesserTests.java
+++ b/main/tests/server/src/com/google/refine/importing/EncodingGuesserTests.java
@@ -15,7 +15,7 @@ import com.google.refine.util.ParsingUtilities;

 public class EncodingGuesserTests {

-    // Guessing isn't as reliable for single-byte encodings, so we focus on a few multi-byte
+    // Guessing isn't as reliable for single-byte encodings, so we focus on a few multibyte
     // non-UTF8 encodings which are still in use (but <1% prevalence on web)
     static String[] ENCODINGS = {
             "big5",
@@ -25,7 +25,7 @@ public class EncodingGuesserTests {
     };

     private static File getTestDir() {
-        String dir = ClassLoader.getSystemResource(ENCODINGS[0] + ".html").getPath();
+        String dir = ClassLoader.getSystemResource(ENCODINGS[0] + ".txt").getPath();
         dir = dir.substring(0, dir.lastIndexOf('/'));
         return new File(dir);
     }
@@ -46,21 +46,29 @@ public class EncodingGuesserTests {
     public void testEncodingGuesser() throws IOException {

         for (String encoding : ENCODINGS) {
-            ImportingJob job = new ImportingJobStub();
-            ObjectNode config = job.getOrCreateDefaultConfig();
-            ObjectNode filesObj = ParsingUtilities.evaluateJsonStringToObjectNode(
-                    String.format("{ \"files\": [ {\"location\": \"%s.txt\"}]}", encoding));
-            JSONUtilities.safePut(config, "retrievalRecord", filesObj);
-
-            EncodingGuesser.guess(job);
-
-            ObjectNode retrievalRecord = job.getRetrievalRecord();
-            assertNotNull(retrievalRecord);
-            ArrayNode fileRecords = JSONUtilities.getArray(retrievalRecord, "files");
-            assertNotNull(fileRecords);
-            assertEquals(fileRecords.size(), 1);
-            ObjectNode record = JSONUtilities.getObjectElement(fileRecords, 0);
-            assertEquals(JSONUtilities.getString(record, "encoding", null).toLowerCase(), encoding);
+            checkEncoding(encoding + ".txt", encoding);
         }
+
+        checkEncoding("example-latin1.tsv", "windows-1252"); // close enough - these overlap a lot
+        checkEncoding("example-utf8.tsv", "utf-8");
+        checkEncoding("csv-with-bom.csv", "utf-8-bom");
+    }
+
+    private void checkEncoding(String filename, String encoding) throws IOException {
+        ImportingJob job = new ImportingJobStub();
+        ObjectNode config = job.getOrCreateDefaultConfig();
+        ObjectNode filesObj = ParsingUtilities.evaluateJsonStringToObjectNode(
+                String.format("{ \"files\": [ {\"location\": \"%s\"}]}", filename));
+        JSONUtilities.safePut(config, "retrievalRecord", filesObj);
+
+        EncodingGuesser.guess(job);
+
+        ObjectNode retrievalRecord = job.getRetrievalRecord();
+        assertNotNull(retrievalRecord);
+        ArrayNode fileRecords = JSONUtilities.getArray(retrievalRecord, "files");
+        assertNotNull(fileRecords);
+        assertEquals(fileRecords.size(), 1);
+        ObjectNode record = JSONUtilities.getObjectElement(fileRecords, 0);
+        assertEquals(JSONUtilities.getString(record, "encoding", null).toLowerCase(), encoding);
     }
 }
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 19fc71ce8..0de1291b2 100644
--- a/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
+++ b/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
@@ -56,7 +56,6 @@ import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
 import org.apache.hc.client5.http.entity.mime.StringBody;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
-import org.checkerframework.checker.units.qual.A;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -110,7 +109,7 @@ public class ImportingUtilitiesTests extends ImporterTest {
         File dirA = new File(tempDir, "a");
         dirA.mkdir();
         File conflicting = new File(dirA, "dummy");
-        conflicting.createNewFile();
+        Assert.assertTrue(conflicting.createNewFile());

         File allocated = ImportingUtilities.allocateFile(dirA, ".././a/dummy");
         Assert.assertEquals(allocated, new File(dirA, "dummy-2"));

Fixes #1241

* UTF-8-BOM to the encoding guesser
* Add support for stripping the BOM to the importer

CSV Byte Order Mark (BOM) support. Fixes #1241