diff --git a/.github/workflows/snapshot_release.yml b/.github/workflows/snapshot_release.yml
index 21ce82ff3..cdd5fbf84 100644
--- a/.github/workflows/snapshot_release.yml
+++ b/.github/workflows/snapshot_release.yml
@@ -24,7 +24,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'
       - id: set-matrix
         run: npm install --save glob@8.1.0 && node main/tests/cypress/build-test-matrix.js >> $GITHUB_OUTPUT
         env:
@@ -60,7 +60,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'

       - name: Restore Tests dependency cache
         uses: actions/cache@v4
diff --git a/benchmark/pom.xml b/benchmark/pom.xml
index 107242029..52e731806 100644
--- a/benchmark/pom.xml
+++ b/benchmark/pom.xml
@@ -58,7 +58,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-compiler-plugin</artifactId>
-        <version>3.10.1</version>
+        <version>3.13.0</version>
         <configuration>
           <compilerVersion>${javac.target}</compilerVersion>
           <source>${javac.target}</source>
@@ -145,7 +145,7 @@
         </plugin>
         <plugin>
           <artifactId>maven-source-plugin</artifactId>
-          <version>3.2.1</version>
+          <version>3.3.1</version>
         </plugin>
         <plugin>
           <artifactId>maven-surefire-plugin</artifactId>
diff --git a/extensions/phonetic/.gitignore b/extensions/phonetic/.gitignore
deleted file mode 100644
index 17b5c9465..000000000
--- a/extensions/phonetic/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-module/MOD-INF/classes/
diff --git a/extensions/phonetic/module/MOD-INF/controller.js b/extensions/phonetic/module/MOD-INF/controller.js
deleted file mode 100644
index 6d652f685..000000000
--- a/extensions/phonetic/module/MOD-INF/controller.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-
-Copyright 2010, Google Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
- */
-
-var html = "text/html";
-var encoding = "UTF-8";
-var ClientSideResourceManager = Packages.com.google.refine.ClientSideResourceManager;
-
-/*
- * Function invoked to initialize the extension.
- */
-function init() {
-  // Register new keyers
-  Packages.com.google.refine.clustering.binning.KeyerFactory.put("daitch-mokotoff", new Packages.org.openrefine.phonetic.keyers.DaitchMokotoffKeyer());
-  Packages.com.google.refine.clustering.binning.KeyerFactory.put("beider-morse", new Packages.org.openrefine.phonetic.keyers.BeiderMorseKeyer());
-
-  // Similarly, we could register new distances like this:
-  // Packages.com.google.refine.clustering.knn.DistanceFactory.put("my-distance", new Packages.org.openrefine.mydistances.MyDistance());
-
-    /*
-     * Resources: just some javascript to load the localized strings
-     */
-    ClientSideResourceManager.addPaths(
-      "project/scripts",
-      module,
-      [
-        "scripts/load-language.js",
-      ]
-    );
-
-}
diff --git a/extensions/phonetic/module/MOD-INF/module.properties b/extensions/phonetic/module/MOD-INF/module.properties
deleted file mode 100644
index e207201d3..000000000
--- a/extensions/phonetic/module/MOD-INF/module.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-name = phonetic
-description = OpenRefine Phonetic Clustering extension
-templating.macros = macros.vm
-requires = core
diff --git a/extensions/phonetic/module/langs/translation-en.json b/extensions/phonetic/module/langs/translation-en.json
deleted file mode 100644
index 5cd9e72e6..000000000
--- a/extensions/phonetic/module/langs/translation-en.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-    "clustering-keyers/beider-morse": "Beider-Morse",
-    "clustering-keyers/daitch-mokotoff": "Daitch-Mokotoff"
-}
diff --git a/extensions/phonetic/module/macros.vm b/extensions/phonetic/module/macros.vm
deleted file mode 100644
index 18a83bfce..000000000
--- a/extensions/phonetic/module/macros.vm
+++ /dev/null
@@ -1,14 +0,0 @@
-#*
-  This file contains common velocity macros used in all .vt files.
-  For Velocity documentation, see:
-
-  http://velocity.apache.org/engine/releases/velocity-1.5/user-guide.html
-*#
-
-#macro( makeAList $list )
-

-    #foreach($item in $list)
-
$item

-    #end
-

-#end
\ No newline at end of file
diff --git a/extensions/phonetic/module/scripts/load-language.js b/extensions/phonetic/module/scripts/load-language.js
deleted file mode 100644
index 2a05202b4..000000000
--- a/extensions/phonetic/module/scripts/load-language.js
+++ /dev/null
@@ -1,1 +0,0 @@
-I18NUtil.init("phonetic");
diff --git a/extensions/phonetic/pom.xml b/extensions/phonetic/pom.xml
deleted file mode 100644
index 438072841..000000000
--- a/extensions/phonetic/pom.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>org.openrefine</groupId>
-  <artifactId>phonetic</artifactId>
-  <packaging>jar</packaging>
-  <version>3.9-SNAPSHOT</version>
-
-  <name>OpenRefine - Phonetic clustering extension</name>
-  <description>Adds a few advanced phonetic clustering methods</description>
-  <url>http://openrefine.org/</url>
-  <parent>
-    <groupId>org.openrefine</groupId>
-    <artifactId>extensions</artifactId>
-    <version>3.9-SNAPSHOT</version>
-  </parent>
-
-  <build>
-    <finalName>openrefine-sample</finalName>
-    <resources>
-      <resource>
-        <directory>src</directory>
-      </resource>
-    </resources>
-    <sourceDirectory>src</sourceDirectory>
-    <testSourceDirectory>tests/src</testSourceDirectory>
-    <outputDirectory>module/MOD-INF/classes</outputDirectory>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-surefire-plugin</artifactId>
-        <version>${surefire.version}</version>
-        <configuration>
-          <suiteXmlFiles>
-            <suiteXmlFile>tests/conf/tests.xml</suiteXmlFile>
-          </suiteXmlFiles>
-        </configuration>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-dependency-plugin</artifactId>
-	<version>${maven-dependency-plugin.version}</version>
-        <executions>
-            <execution>
-            <phase>compile</phase>
-            <goals>
-                <goal>copy-dependencies</goal>
-            </goals>
-            <configuration>
-                <outputDirectory>module/MOD-INF/lib</outputDirectory>
-                <includeScope>runtime</includeScope>
-            </configuration>
-            </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-
-  <dependencies>
-     <dependency>
-      <groupId>${project.groupId}</groupId>
-      <artifactId>main</artifactId>
-      <version>${project.version}</version>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
-      <groupId>javax.servlet</groupId>
-      <artifactId>javax.servlet-api</artifactId>
-      <version>${servlet-api.version}</version>
-      <scope>provided</scope>
-    </dependency>
-
-
-
-    <dependency>
-      <groupId>org.testng</groupId>
-      <artifactId>testng</artifactId>
-      <version>${testng.version}</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-slf4j-impl</artifactId>
-      <version>${log4j.version}</version>
-      <scope>test</scope>
-    </dependency>
-
-  </dependencies>
-
-</project>
-
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 8243ffc22..592a15135 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -23,7 +23,6 @@
         <module>database</module>
         <module>gdata</module>
         <module>pc-axis</module>
-        <module>phonetic</module>

   </modules>

@@ -50,7 +49,7 @@
       <plugin>
         <groupId>org.jacoco</groupId>
         <artifactId>jacoco-maven-plugin</artifactId>
-        <version>0.8.11</version>
+        <version>0.8.12</version>
         <executions>
             <execution>
                 <id>prepare-agent</id>
diff --git a/extensions/phonetic/src/org/openrefine/phonetic/keyers/BeiderMorseKeyer.java b/main/src/com/google/refine/clustering/binning/BeiderMorseKeyer.java
similarity index 91%
rename from extensions/phonetic/src/org/openrefine/phonetic/keyers/BeiderMorseKeyer.java
rename to main/src/com/google/refine/clustering/binning/BeiderMorseKeyer.java
index af2759231..eb0ea843a 100644
--- a/extensions/phonetic/src/org/openrefine/phonetic/keyers/BeiderMorseKeyer.java
+++ b/main/src/com/google/refine/clustering/binning/BeiderMorseKeyer.java
@@ -1,11 +1,9 @@

-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;

 import org.apache.commons.codec.EncoderException;
 import org.apache.commons.codec.language.bm.BeiderMorseEncoder;

-import com.google.refine.clustering.binning.Keyer;
-
 public class BeiderMorseKeyer extends Keyer {

     protected BeiderMorseEncoder encoder = new BeiderMorseEncoder();
diff --git a/extensions/phonetic/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyer.java b/main/src/com/google/refine/clustering/binning/DaitchMokotoffKeyer.java
similarity index 77%
rename from extensions/phonetic/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyer.java
rename to main/src/com/google/refine/clustering/binning/DaitchMokotoffKeyer.java
index 8d7b1879e..96ba20b6c 100644
--- a/extensions/phonetic/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyer.java
+++ b/main/src/com/google/refine/clustering/binning/DaitchMokotoffKeyer.java
@@ -1,10 +1,8 @@

-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;

 import org.apache.commons.codec.language.DaitchMokotoffSoundex;

-import com.google.refine.clustering.binning.Keyer;
-
 public class DaitchMokotoffKeyer extends Keyer {

     protected DaitchMokotoffSoundex encoder = new DaitchMokotoffSoundex();
diff --git a/main/src/com/google/refine/exporters/ExporterRegistry.java b/main/src/com/google/refine/exporters/ExporterRegistry.java
index 00c4e3092..10c777aa2 100644
--- a/main/src/com/google/refine/exporters/ExporterRegistry.java
+++ b/main/src/com/google/refine/exporters/ExporterRegistry.java
@@ -36,29 +36,10 @@ package com.google.refine.exporters;
 import java.util.HashMap;
 import java.util.Map;

-import com.google.refine.exporters.sql.SqlExporter;
-
-abstract public class ExporterRegistry {
+public class ExporterRegistry {

     static final private Map<String, Exporter> s_formatToExporter = new HashMap<String, Exporter>();

-    static {
-        s_formatToExporter.put("csv", new CsvExporter());
-        s_formatToExporter.put("tsv", new CsvExporter('\t'));
-        s_formatToExporter.put("*sv", new CsvExporter());
-
-        s_formatToExporter.put("xls", new XlsExporter(false));
-        s_formatToExporter.put("xlsx", new XlsExporter(true));
-
-        s_formatToExporter.put("ods", new OdsExporter());
-
-        s_formatToExporter.put("html", new HtmlTableExporter());
-
-        s_formatToExporter.put("template", new TemplatingExporter());
-
-        s_formatToExporter.put("sql", new SqlExporter());
-    }
-
     static public void registerExporter(String format, Exporter exporter) {
         s_formatToExporter.put(format.toLowerCase(), exporter);
     }
diff --git a/main/webapp/modules/core/MOD-INF/controller.js b/main/webapp/modules/core/MOD-INF/controller.js
index 2bf658289..344b1dfa4 100644
--- a/main/webapp/modules/core/MOD-INF/controller.js
+++ b/main/webapp/modules/core/MOD-INF/controller.js
@@ -319,6 +319,19 @@ function registerImporting() {
   );
 }

+function registerExporters() {
+   var ER = Packages.com.google.refine.exporters.ExporterRegistry;
+   ER.registerExporter("csv", new Packages.com.google.refine.exporters.CsvExporter());
+   ER.registerExporter("tsv", new Packages.com.google.refine.exporters.CsvExporter('\t'.charAt(0)));
+   ER.registerExporter("*sv", new Packages.com.google.refine.exporters.CsvExporter());
+   ER.registerExporter("xls", new Packages.com.google.refine.exporters.XlsExporter(false));
+   ER.registerExporter("xlsx", new Packages.com.google.refine.exporters.XlsExporter(true));
+   ER.registerExporter("ods", new Packages.com.google.refine.exporters.OdsExporter());
+   ER.registerExporter("html", new Packages.com.google.refine.exporters.HtmlTableExporter());
+   ER.registerExporter("template", new Packages.com.google.refine.exporters.TemplatingExporter());
+   ER.registerExporter("sql", new Packages.com.google.refine.exporters.sql.SqlExporter());
+}
+
 function registerDistances() {
    var DF = Packages.com.google.refine.clustering.knn.DistanceFactory;
    var VicinoDistance = Packages.com.google.refine.clustering.knn.VicinoDistance;
@@ -332,6 +345,8 @@ function registerKeyers() {
    KF.put("ngram-fingerprint", new Packages.com.google.refine.clustering.binning.NGramFingerprintKeyer());
    KF.put("metaphone3", new Packages.com.google.refine.clustering.binning.Metaphone3Keyer());
    KF.put("cologne-phonetic", new Packages.com.google.refine.clustering.binning.ColognePhoneticKeyer());
+   KF.put("daitch-mokotoff", new Packages.com.google.refine.clustering.binning.DaitchMokotoffKeyer());
+   KF.put("beider-morse", new Packages.com.google.refine.clustering.binning.BeiderMorseKeyer());
 }

 /*
@@ -343,6 +358,7 @@ function init() {
   registerCommands();
   registerOperations();
   registerImporting();
+  registerExporters();
   registerDistances();
   registerKeyers();

diff --git a/main/webapp/modules/core/langs/translation-en.json b/main/webapp/modules/core/langs/translation-en.json
index fec7180b5..c636782c8 100644
--- a/main/webapp/modules/core/langs/translation-en.json
+++ b/main/webapp/modules/core/langs/translation-en.json
@@ -218,6 +218,8 @@
     "clustering-keyers/ngram-fingerprint": "n-Gram fingerprint",
     "clustering-keyers/metaphone3": "Metaphone3",
     "clustering-keyers/cologne-phonetic": "Cologne phonetic",
+    "clustering-keyers/beider-morse": "Beider-Morse",
+    "clustering-keyers/daitch-mokotoff": "Daitch-Mokotoff",
     "core-dialogs/distance-fun": "Distance function",
     "clustering-distances/levenshtein": "Levenshtein",
     "clustering-distances/ppm": "PPM",
diff --git a/main/webapp/modules/core/scripts/views/data-table/cell-ui.js b/main/webapp/modules/core/scripts/views/data-table/cell-ui.js
index 3d1ac1c74..c1a8d56c1 100644
--- a/main/webapp/modules/core/scripts/views/data-table/cell-ui.js
+++ b/main/webapp/modules/core/scripts/views/data-table/cell-ui.js
@@ -85,7 +85,11 @@ DataTableCellUI.prototype._render = function() {

   var renderedCell = undefined;
   for (let record of CellRendererRegistry.renderers) {
-    renderedCell = record.renderer.render(this._rowIndex, this._cellIndex, cell, this);
+    try {
+      renderedCell = record.renderer.render(this._rowIndex, this._cellIndex, cell, this);
+    } catch (e) {
+      continue;
+    }
     if (renderedCell) {
       break;
     }
diff --git a/pom.xml b/pom.xml
index fcd4be35e..542db8eb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,11 +100,11 @@
     <httpcore.version>4.4.16</httpcore.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.16.0</commons-io.version>
+    <commons-io.version>2.16.1</commons-io.version>
     <guava.version>33.1.0-jre</guava.version>
     <jaxb-api.version>2.3.1</jaxb-api.version>
     <juniversalchardet.version>2.4.0</juniversalchardet.version>
-    <testng.version>7.9.0</testng.version>
+    <testng.version>7.10.1</testng.version>
     <httpmime.version>4.5.14</httpmime.version>
     <owasp-encoder.version>1.2.3</owasp-encoder.version>
     <jasypt.version>1.9.3</jasypt.version>
@@ -112,11 +112,11 @@

     <build-helper-maven-plugin.version>3.5.0</build-helper-maven-plugin.version>
-    <maven-compiler-plugin.version>3.10.1</maven-compiler-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.2.0</exec-maven-plugin.version>
     <maven-dependency-plugin.version>3.6.1</maven-dependency-plugin.version>
-    <jacoco-maven-plugin.version>0.8.11</jacoco-maven-plugin.version>
+    <jacoco-maven-plugin.version>0.8.12</jacoco-maven-plugin.version>
     <coveralls-maven-plugin.version>4.3.0</coveralls-maven-plugin.version>
     <surefire.version>3.2.5</surefire.version>
     <maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
@@ -263,7 +263,7 @@

         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-source-plugin</artifactId>
-        <version>3.2.1</version>
+        <version>3.3.1</version>
         <executions>
           <execution>
             <id>attach-sources</id>

diff --git a/.github/workflows/pull_request_e2e.yml b/.github/workflows/pull_request_e2e.yml
index dca92d736..fb09561a5 100644
--- a/.github/workflows/pull_request_e2e.yml
+++ b/.github/workflows/pull_request_e2e.yml
@@ -26,7 +26,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'
       - id: set-matrix
         run: npm install --save glob@8.1.0 && node main/tests/cypress/build-test-matrix.js >> $GITHUB_OUTPUT
         env:
@@ -58,7 +58,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'

       - name: Restore Tests dependency cache
         uses: actions/cache@v4
diff --git a/extensions/phonetic/tests/conf/tests.xml b/extensions/phonetic/tests/conf/tests.xml
deleted file mode 100644
index 7ad8efc22..000000000
--- a/extensions/phonetic/tests/conf/tests.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >
-
-<suite name="Wikidata extension">
-  <test name="tests">
-    <groups>
-      <run>
-        <exclude name="broken" />
-      </run>
-    </groups>
-    <packages>
-      <package name="org.openrefine.phonetic.*" />
-    </packages>
-  </test>
-</suite>
diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index 6d2dd4f0e..800370113 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -6,7 +6,7 @@
     "author": "OpenRefine",
     "private": true,
     "engines": {
-        "node": ">=16.0.0",
+        "node": ">=20.0.0",
         "npm":  ">=8.11.0",
         "yarn": ">=1.22.15"
     },
@@ -16,11 +16,11 @@
         "lint": "prettier --check . && eslint ."
     },
     "dependencies": {
-        "cypress": "13.7.1",
+        "cypress": "13.7.2",
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
-        "eslint": "^8.57.0",
+        "eslint": "^9.0.0",
         "eslint-config-google": "^0.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-cypress": "^2.15.1",
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 9264ebdc2..17be768c5 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -51,32 +51,32 @@
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
   integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==

-"@eslint/eslintrc@^2.1.4":
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
-  integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
+"@eslint/eslintrc@^3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.0.2.tgz#36180f8e85bf34d2fe3ccc2261e8e204a411ab4e"
+  integrity sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.6.0"
-    globals "^13.19.0"
+    espree "^10.0.1"
+    globals "^14.0.0"
     ignore "^5.2.0"
     import-fresh "^3.2.1"
     js-yaml "^4.1.0"
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"

-"@eslint/js@8.57.0":
-  version "8.57.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
-  integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
+"@eslint/js@9.0.0":
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691"
+  integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==

-"@humanwhocodes/config-array@^0.11.14":
-  version "0.11.14"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
-  integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==
+"@humanwhocodes/config-array@^0.12.3":
+  version "0.12.3"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.12.3.tgz#a6216d90f81a30bedd1d4b5d799b47241f318072"
+  integrity sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g==
   dependencies:
-    "@humanwhocodes/object-schema" "^2.0.2"
+    "@humanwhocodes/object-schema" "^2.0.3"
     debug "^4.3.1"
     minimatch "^3.0.5"

@@ -85,10 +85,10 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
   integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==

-"@humanwhocodes/object-schema@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917"
-  integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==
+"@humanwhocodes/object-schema@^2.0.3":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
+  integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==

 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
@@ -138,20 +138,15 @@
   dependencies:
     "@types/node" "*"

-"@ungap/structured-clone@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
-  integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
-
 acorn-jsx@^5.3.2:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==

-acorn@^8.9.0:
-  version "8.9.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59"
-  integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==
+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==

 aggregate-error@^3.0.0:
   version "3.1.0"
@@ -436,10 +431,10 @@ 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.7.1:
-  version "13.7.1"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.1.tgz#d1208eb04efd46ef52a30480a5da71a03373261a"
-  integrity sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w==
+cypress@13.7.2:
+  version "13.7.2"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.2.tgz#61e841382abb20e0a9a063086ee0d850af3ef6bc"
+  integrity sha512-FF5hFI5wlRIHY8urLZjJjj/YvfCBrRpglbZCLr/cYcL9MdDe0+5usa8kTIrDHthlEc9lwihbkb5dmwqBDNS2yw==
   dependencies:
     "@cypress/request" "^3.0.0"
     "@cypress/xvfb" "^1.2.4"
@@ -520,13 +515,6 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=

-doctrine@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz"
-  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
-  dependencies:
-    esutils "^2.0.2"
-
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz"
@@ -581,54 +569,55 @@ eslint-plugin-cypress@^2.15.1:
   dependencies:
     globals "^13.20.0"

-eslint-scope@^7.2.2:
-  version "7.2.2"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
-  integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
+eslint-scope@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc"
+  integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"

-eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
+eslint-visitor-keys@^3.3.0:
   version "3.4.3"
   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.57.0:
-  version "8.57.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
-  integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
+eslint-visitor-keys@^4.0.0:
+  version "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.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.0.0.tgz#6270548758e390343f78c8afd030566d86927d40"
+  integrity sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.6.1"
-    "@eslint/eslintrc" "^2.1.4"
-    "@eslint/js" "8.57.0"
-    "@humanwhocodes/config-array" "^0.11.14"
+    "@eslint/eslintrc" "^3.0.2"
+    "@eslint/js" "9.0.0"
+    "@humanwhocodes/config-array" "^0.12.3"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
-    "@ungap/structured-clone" "^1.2.0"
     ajv "^6.12.4"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.3.2"
-    doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^7.2.2"
-    eslint-visitor-keys "^3.4.3"
-    espree "^9.6.1"
+    eslint-scope "^8.0.1"
+    eslint-visitor-keys "^4.0.0"
+    espree "^10.0.1"
     esquery "^1.4.2"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
-    file-entry-cache "^6.0.1"
+    file-entry-cache "^8.0.0"
     find-up "^5.0.0"
     glob-parent "^6.0.2"
-    globals "^13.19.0"
     graphemer "^1.4.0"
     ignore "^5.2.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
     is-path-inside "^3.0.3"
-    js-yaml "^4.1.0"
     json-stable-stringify-without-jsonify "^1.0.1"
     levn "^0.4.1"
     lodash.merge "^4.6.2"
@@ -638,14 +627,14 @@ eslint@^8.57.0:
     strip-ansi "^6.0.1"
     text-table "^0.2.0"

-espree@^9.6.0, espree@^9.6.1:
-  version "9.6.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
-  integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+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==
   dependencies:
-    acorn "^8.9.0"
+    acorn "^8.11.3"
     acorn-jsx "^5.3.2"
-    eslint-visitor-keys "^3.4.1"
+    eslint-visitor-keys "^4.0.0"

 esquery@^1.4.2:
   version "1.4.2"
@@ -760,12 +749,12 @@ figures@^3.2.0:
   dependencies:
     escape-string-regexp "^1.0.5"

-file-entry-cache@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
-  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+file-entry-cache@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
+  integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
   dependencies:
-    flat-cache "^3.0.4"
+    flat-cache "^4.0.0"

 find-up@^5.0.0:
   version "5.0.0"
@@ -775,18 +764,18 @@ find-up@^5.0.0:
     locate-path "^6.0.0"
     path-exists "^4.0.0"

-flat-cache@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz"
-  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+flat-cache@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
+  integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
   dependencies:
-    flatted "^3.1.0"
-    rimraf "^3.0.2"
+    flatted "^3.2.9"
+    keyv "^4.5.4"

-flatted@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz"
-  integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
+flatted@^3.2.9:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
+  integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==

 forever-agent@~0.6.1:
   version "0.6.1"
@@ -879,13 +868,18 @@ global-dirs@^3.0.0:
   dependencies:
     ini "2.0.0"

-globals@^13.19.0, globals@^13.20.0:
+globals@^13.20.0:
   version "13.20.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82"
   integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==
   dependencies:
     type-fest "^0.20.2"

+globals@^14.0.0:
+  version "14.0.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
 graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.6"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
@@ -1047,6 +1041,11 @@ jsbn@~0.1.0:
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=

+json-buffer@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
@@ -1086,6 +1085,13 @@ jsprim@^2.0.2:
     json-schema "0.4.0"
     verror "1.10.0"

+keyv@^4.5.4:
+  version "4.5.4"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+  integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+  dependencies:
+    json-buffer "3.0.1"
+
 lazy-ass@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz"
@@ -1393,7 +1399,7 @@ reusify@^1.0.4:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==

-rimraf@^3.0.0, rimraf@^3.0.2:
+rimraf@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz"
   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
diff --git a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/BeiderMorseKeyerTest.java b/main/tests/server/src/com/google/refine/clustering/binning/BeiderMorseKeyerTest.java
similarity index 83%
rename from extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/BeiderMorseKeyerTest.java
rename to main/tests/server/src/com/google/refine/clustering/binning/BeiderMorseKeyerTest.java
index adca635f8..85b5b4666 100644
--- a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/BeiderMorseKeyerTest.java
+++ b/main/tests/server/src/com/google/refine/clustering/binning/BeiderMorseKeyerTest.java
@@ -1,12 +1,10 @@

-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;

 import static org.testng.Assert.assertEquals;

 import org.testng.annotations.Test;

-import com.google.refine.clustering.binning.Keyer;
-
 public class BeiderMorseKeyerTest {

     Keyer keyer = new BeiderMorseKeyer();
diff --git a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyerTest.java b/main/tests/server/src/com/google/refine/clustering/binning/DaitchMokotoffKeyerTest.java
similarity index 84%
rename from extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyerTest.java
rename to main/tests/server/src/com/google/refine/clustering/binning/DaitchMokotoffKeyerTest.java
index 50129ff1b..c699803e3 100644
--- a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyerTest.java
+++ b/main/tests/server/src/com/google/refine/clustering/binning/DaitchMokotoffKeyerTest.java
@@ -1,12 +1,10 @@

-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;

 import static org.testng.Assert.assertEquals;

 import org.testng.annotations.Test;

-import com.google.refine.clustering.binning.Keyer;
-
 public class DaitchMokotoffKeyerTest {

     protected Keyer keyer = new DaitchMokotoffKeyer();

Merge phonetic extension into the main app


diff --git a/main/src/com/google/refine/expr/functions/Length.java b/main/src/com/google/refine/expr/functions/Length.java
index d7fde5db9..6ca73c740 100644
--- a/main/src/com/google/refine/expr/functions/Length.java
+++ b/main/src/com/google/refine/expr/functions/Length.java
@@ -37,6 +37,8 @@ import java.util.Collection;
 import java.util.Properties;

 import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
 import com.google.refine.expr.EvalError;
 import com.google.refine.expr.HasFieldsList;
 import com.google.refine.grel.ControlFunctionRegistry;
@@ -61,6 +63,8 @@ public class Length implements Function {
                     return ((HasFieldsList) v).length();
                 } else if (v instanceof ArrayNode) {
                     return ((ArrayNode) v).size();
+                } else if (v instanceof ObjectNode) {
+                    return ((ObjectNode) v).size();
                 } else {
                     String s = (v instanceof String ? (String) v : v.toString());
                     return s.length();
diff --git a/pom.xml b/pom.xml
index 7522c027c..4f5ef1c2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,7 +81,7 @@
     <okhttp.version>4.10.0</okhttp.version>
     <jena.version>4.8.0</jena.version>
     <poi.version>5.2.3</poi.version>
-    <commons-lang3.version>3.12.0</commons-lang3.version>
+    <commons-lang3.version>3.13.0</commons-lang3.version>
     <commons-fileupload.version>1.5</commons-fileupload.version>
     <commons-collections.version>3.2.2</commons-collections.version>
     <commons-text.version>1.10.0</commons-text.version>

diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index a9a8bbea0..e480e5008 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": "^8.45.0",
+        "eslint": "^8.46.0",
         "eslint-config-google": "^0.14.0",
         "eslint-config-prettier": "^8.9.0",
         "eslint-plugin-cypress": "^2.13.3",
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 110041631..135b33a3e 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -46,15 +46,15 @@
   dependencies:
     eslint-visitor-keys "^3.3.0"

-"@eslint-community/regexpp@^4.4.0":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403"
-  integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==
+"@eslint-community/regexpp@^4.6.1":
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
+  integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==

-"@eslint/eslintrc@^2.1.0":
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d"
-  integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==
+"@eslint/eslintrc@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93"
+  integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
@@ -66,10 +66,10 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"

-"@eslint/js@8.44.0":
-  version "8.44.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af"
-  integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==
+"@eslint/js@^8.46.0":
+  version "8.46.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6"
+  integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==

 "@humanwhocodes/config-array@^0.11.10":
   version "0.11.10"
@@ -161,7 +161,7 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"

-ajv@^6.10.0, ajv@^6.12.4:
+ajv@^6.12.4:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -581,40 +581,40 @@ eslint-plugin-cypress@^2.13.3:
   dependencies:
     globals "^11.12.0"

-eslint-scope@^7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
-  integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
+eslint-scope@^7.2.2:
+  version "7.2.2"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
+  integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"

-eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
-  integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2:
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f"
+  integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==

-eslint@^8.45.0:
-  version "8.45.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.45.0.tgz#bab660f90d18e1364352c0a6b7c6db8edb458b78"
-  integrity sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==
+eslint@^8.46.0:
+  version "8.46.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552"
+  integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
-    "@eslint-community/regexpp" "^4.4.0"
-    "@eslint/eslintrc" "^2.1.0"
-    "@eslint/js" "8.44.0"
+    "@eslint-community/regexpp" "^4.6.1"
+    "@eslint/eslintrc" "^2.1.1"
+    "@eslint/js" "^8.46.0"
     "@humanwhocodes/config-array" "^0.11.10"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
-    ajv "^6.10.0"
+    ajv "^6.12.4"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.3.2"
     doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^7.2.0"
-    eslint-visitor-keys "^3.4.1"
-    espree "^9.6.0"
+    eslint-scope "^7.2.2"
+    eslint-visitor-keys "^3.4.2"
+    espree "^9.6.1"
     esquery "^1.4.2"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
@@ -637,10 +637,10 @@ eslint@^8.45.0:
     strip-ansi "^6.0.1"
     text-table "^0.2.0"

-espree@^9.6.0:
-  version "9.6.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f"
-  integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==
+espree@^9.6.0, espree@^9.6.1:
+  version "9.6.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
+  integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
   dependencies:
     acorn "^8.9.0"
     acorn-jsx "^5.3.2"
diff --git a/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java b/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java
index 3e04d14d6..14bc957bd 100644
--- a/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java
+++ b/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java
@@ -27,9 +27,35 @@

 package com.google.refine.expr.functions;

+import static org.testng.Assert.assertEquals;
+
+import java.util.Properties;
+
 import org.testng.annotations.Test;

-import com.google.refine.util.TestUtils;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import com.google.refine.RefineTest;
+import com.google.refine.grel.Function;
+import com.google.refine.util.ParsingUtilities;
+
+public class LengthTests extends RefineTest {
+
+    static Properties bindings = new Properties();
+
+    @Test
+    public void testLength() throws JsonProcessingException {
+        Function f = new Length();
+        assertEquals(f.call(bindings, new Object[] { Long.valueOf(123) }), 3);
+        assertEquals(f.call(bindings, new Object[] { Double.valueOf(123.1) }), 5);
+        assertEquals(f.call(bindings, new Object[] { "12345" }), 5);
+
+        ArrayNode array = (ArrayNode) ParsingUtilities.mapper.readTree("[1,2,3]");
+        assertEquals(f.call(bindings, new Object[] { array }), 3);

-public class LengthTests {
+        ObjectNode dict = (ObjectNode) ParsingUtilities.mapper.readTree("{\"a\": \"b\", \"c\": \"d\"}");
+        assertEquals(f.call(bindings, new Object[] { dict }), 2);
+    }
 }

Also add tests for other uses of length() function.

Fixes #5991

length() returns # of keys in object. Fixes #5991

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/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/extensions/wikibase/tests/src/org/openrefine/wikibase/commands/PreviewWikibaseSchemaCommandTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/commands/PreviewWikibaseSchemaCommandTest.java
index e45f0e019..0b9220d15 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/commands/PreviewWikibaseSchemaCommandTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/commands/PreviewWikibaseSchemaCommandTest.java
@@ -30,6 +30,7 @@ import static org.testng.Assert.assertEquals;

 import java.io.IOException;
 import java.io.Serializable;
+import java.util.Locale;

 import javax.servlet.ServletException;

@@ -40,6 +41,7 @@ import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;

+import com.google.refine.util.LocaleUtils;
 import com.google.refine.util.ParsingUtilities;

 import org.openrefine.wikibase.testing.TestingData;
@@ -48,6 +50,8 @@ import org.openrefine.wikibase.utils.EntityCacheStub;

 public class PreviewWikibaseSchemaCommandTest extends SchemaCommandTest {

+    private String localeSetting;
+
     @BeforeMethod
     public void SetUp() {
         command = new PreviewWikibaseSchemaCommand();
@@ -121,6 +125,17 @@ public class PreviewWikibaseSchemaCommandTest extends SchemaCommandTest {
                 "{\"code\":\"error\",\"message\":\"Wikibase manifest could not be parsed. Error message: invalid manifest format\"}");
     }

+    @BeforeMethod
+    public void setLocale() {
+        localeSetting = Locale.getDefault().getLanguage();
+        LocaleUtils.setLocale("en");
+    }
+
+    @AfterMethod
+    public void unsetLocale() {
+        LocaleUtils.setLocale(localeSetting);
+    }
+
     @Test
     public void testWarningData() throws Exception {

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 #6911

Changes proposed in this pull request:
- Set language before test as the test validates label which is language specific
-
-

wikibase: Test failure when run on non-English locales


diff --git a/extensions/wikibase/src/org/openrefine/wikibase/editing/EditBatchProcessor.java b/extensions/wikibase/src/org/openrefine/wikibase/editing/EditBatchProcessor.java
index 4a227b8a6..a027f7104 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/editing/EditBatchProcessor.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/editing/EditBatchProcessor.java
@@ -29,6 +29,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.OptionalLong;
 import java.util.Set;
@@ -39,6 +40,8 @@ import org.slf4j.LoggerFactory;
 import org.wikidata.wdtk.datamodel.interfaces.EntityDocument;
 import org.wikidata.wdtk.datamodel.interfaces.EntityIdValue;
 import org.wikidata.wdtk.datamodel.interfaces.EntityUpdate;
+import org.wikidata.wdtk.datamodel.interfaces.LabeledDocument;
+import org.wikidata.wdtk.datamodel.interfaces.MonolingualTextValue;
 import org.wikidata.wdtk.wikibaseapi.ApiConnection;
 import org.wikidata.wdtk.wikibaseapi.EditingResult;
 import org.wikidata.wdtk.wikibaseapi.WikibaseDataEditor;
@@ -183,14 +186,19 @@ public class EditBatchProcessor {
                 // New entities
                 ReconEntityIdValue newCell = (ReconEntityIdValue) update.getEntityId();
                 EntityIdValue createdDocId;
+                String name = "";
                 if (update instanceof MediaInfoEdit) {
                     MediaFileUtils mediaFileUtils = new MediaFileUtils(connection);
                     createdDocId = ((MediaInfoEdit) update).uploadNewFile(editor, mediaFileUtils, summary, tags, filePageWaitTime,
                             filePageMaxWaitTime);
+                    name = "File:".concat(((MediaInfoEdit) update).getFileName());
                 } else {
-                    createdDocId = editor.createEntityDocument(update.toNewEntity(), summary, tags).getEntityId();
+                    LabeledDocument labeledDocument = (LabeledDocument) editor.createEntityDocument(update.toNewEntity(), summary, tags);
+                    createdDocId = labeledDocument.getEntityId();
+                    name = getDocumentLabel(labeledDocument.getLabels(), createdDocId.getId());
                 }
                 library.setId(newCell.getReconInternalId(), createdDocId.getId());
+                library.setName(newCell.getReconInternalId(), name);
                 newEntityUrl = createdDocId.getSiteIri() + createdDocId.getId();
             } else {
                 // Existing entities
@@ -372,4 +380,22 @@ public class EditBatchProcessor {
         batchCursor = 0;
     }

+    private String getDocumentLabel(Map<String, MonolingualTextValue> labels, String defaultName) {
+        if (labels == null || labels.isEmpty()) {
+            return defaultName;
+        }
+        String docLabel = null;
+        String defaultLanguage = Locale.getDefault().getLanguage();
+        for (Map.Entry<String, MonolingualTextValue> entry : labels.entrySet()) {
+            String language = entry.getKey();
+            MonolingualTextValue labelValue = entry.getValue();
+            if (defaultLanguage.equals(language)) {
+                return labelValue.getText();
+            }
+            if (docLabel == null) {
+                docLabel = labelValue.getText();
+            }
+        }
+        return docLabel == null ? defaultName : docLabel;
+    }
 }
diff --git a/extensions/wikibase/src/org/openrefine/wikibase/editing/NewEntityLibrary.java b/extensions/wikibase/src/org/openrefine/wikibase/editing/NewEntityLibrary.java
index 696b1df6c..020a3087b 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/editing/NewEntityLibrary.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/editing/NewEntityLibrary.java
@@ -50,15 +50,19 @@ import com.google.refine.model.recon.StandardReconConfig;
  */
 public class NewEntityLibrary {

-    private Map<Long, String> map;
+    private Map<Long, String> reconToEntityId;
+    private Map<Long, String> reconToName;

     public NewEntityLibrary() {
-        map = new HashMap<>();
+        reconToEntityId = new HashMap<>();
+        reconToName = new HashMap<>();
     }

     @JsonCreator
-    public NewEntityLibrary(@JsonProperty("qidMap") Map<Long, String> map) {
-        this.map = map;
+    public NewEntityLibrary(@JsonProperty("qidMap") Map<Long, String> reconToEntityId,
+            @JsonProperty("nameMap") Map<Long, String> reconToName) {
+        this.reconToEntityId = reconToEntityId;
+        this.reconToName = reconToName;
     }

     /**
@@ -69,7 +73,11 @@ public class NewEntityLibrary {
      * @return the id (or null if unallocated yet)
      */
     public String getId(long id) {
-        return map.get(id);
+        return reconToEntityId.get(id);
+    }
+
+    public String getName(long id) {
+        return reconToName.get(id);
     }

     /**
@@ -81,7 +89,11 @@ public class NewEntityLibrary {
      *            : the associated id returned by Wikibase
      */
     public void setId(long id, String returnedId) {
-        map.put(id, returnedId);
+        reconToEntityId.put(id, returnedId);
+    }
+
+    public void setName(long id, String name) {
+        reconToName.put(id, name);
     }

     /**
@@ -110,15 +122,17 @@ public class NewEntityLibrary {
                 Recon recon = cell.recon;
                 boolean changed = false;
                 if (Recon.Judgment.New.equals(recon.judgment) && !reset
-                        && map.containsKey(recon.id)) {
+                        && reconToEntityId.containsKey(recon.id)) {
                     recon.judgment = Recon.Judgment.Matched;
-                    recon.match = new ReconCandidate(map.get(recon.id), cell.value.toString(),
+                    recon.match = new ReconCandidate(reconToEntityId.get(recon.id).toString(),
+                            reconToName != null && reconToName.get(recon.id) != null ? reconToName.get(recon.id).toString()
+                                    : cell.value.toString(),
                             new String[0], 100);
                     recon.addCandidate(recon.match);
                     changed = true;

                 } else if (Recon.Judgment.Matched.equals(recon.judgment) && reset
-                        && map.containsKey(recon.id)) {
+                        && reconToEntityId.containsKey(recon.id)) {
                     recon.judgment = Recon.Judgment.New;
                     if (recon.candidates != null) {
                         recon.candidates.remove(recon.candidates.size() - 1);
@@ -155,7 +169,12 @@ public class NewEntityLibrary {
      */
     @JsonProperty("qidMap")
     public Map<Long, String> getIdMap() {
-        return map;
+        return reconToEntityId;
+    }
+
+    @JsonProperty("nameMap")
+    public Map<Long, String> getNameMap() {
+        return reconToName;
     }

     @Override
@@ -164,16 +183,19 @@ public class NewEntityLibrary {
             return false;
         }
         NewEntityLibrary otherLibrary = (NewEntityLibrary) other;
-        return map.equals(otherLibrary.getIdMap());
+        return reconToEntityId.equals(otherLibrary.getIdMap()) && (reconToName != null && reconToName.equals(otherLibrary.getNameMap()));
     }

     @Override
     public int hashCode() {
-        return map.hashCode();
+        return reconToEntityId.hashCode() + (reconToName != null ? reconToName.hashCode() : 0);
     }

     @Override
     public String toString() {
-        return map.toString();
+        return "{" +
+                "reconToEntityId=" + reconToEntityId +
+                ", reconToName=" + reconToName +
+                '}';
     }
 }
diff --git a/extensions/wikibase/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperation.java b/extensions/wikibase/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperation.java
index 2bdb3fd83..c8f0e4b17 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperation.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperation.java
@@ -210,6 +210,7 @@ public class PerformWikibaseEditsOperation extends EngineDependentOperation {
             if (newEntityLibrary != null) {
                 writer.write("newItems=");
                 ObjectMapper mapper = new ObjectMapper();
+                mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
                 writer.write(mapper.writeValueAsString(newEntityLibrary) + "\n");
             }
             writer.write("/ec/\n"); // end of change
@@ -287,6 +288,7 @@ public class PerformWikibaseEditsOperation extends EngineDependentOperation {
                 onDone.add(createFacetAction);

             }
+
         }

         @Override
diff --git a/extensions/wikibase/src/org/openrefine/wikibase/updates/MediaInfoEdit.java b/extensions/wikibase/src/org/openrefine/wikibase/updates/MediaInfoEdit.java
index dede650c5..a930b21fa 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/updates/MediaInfoEdit.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/updates/MediaInfoEdit.java
@@ -314,7 +314,9 @@ public class MediaInfoEdit extends LabeledStatementEntityEdit {
         ReconEntityIdValue reconEntityIdValue = (ReconEntityIdValue) id;
         MediaInfoIdValue mid = response.getMid(mediaFileUtils.getApiConnection(), reconEntityIdValue.getRecon().identifierSpace);
         NewEntityLibrary library = new NewEntityLibrary();
+        String label = this.fileName.startsWith("File:") ? this.fileName : "File:".concat(this.fileName);
         library.setId(reconEntityIdValue.getReconInternalId(), mid.getId());
+        library.setName(reconEntityIdValue.getReconInternalId(), label);
         ReconEntityRewriter rewriter = new ReconEntityRewriter(library, id);
         try {
             MediaInfoEdit rewritten = (MediaInfoEdit) rewriter.rewrite(this);

diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/EditBatchProcessorTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/EditBatchProcessorTest.java
index 26d959216..e3aa9e7b3 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/EditBatchProcessorTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/EditBatchProcessorTest.java
@@ -143,6 +143,7 @@ public class EditBatchProcessorTest extends WikidataRefineTest {

         NewEntityLibrary expectedLibrary = new NewEntityLibrary();
         expectedLibrary.setId(1234L, "Q1234");
+        expectedLibrary.setName(1234L, "better label");
         assertEquals(expectedLibrary, library);
     }

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 b25f029fe..3edcbf01a 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/NewEntityLibraryTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/NewEntityLibraryTest.java
@@ -49,7 +49,9 @@ public class NewEntityLibraryTest extends WikidataRefineTest {
     public void setUp() {
         library = new NewEntityLibrary();
         library.setId(1234L, "Q345");
+        library.setName(1234L, "new uni");
         library.setId(3289L, "Q384");
+        library.setName(3289L, "University of Ljubljana");
     }

     @Test
@@ -87,7 +89,7 @@ public class NewEntityLibraryTest extends WikidataRefineTest {
     @Test
     public void testSerialize() {
         JacksonSerializationTest.canonicalSerialization(NewEntityLibrary.class, library,
-                "{\"qidMap\":{\"1234\":\"Q345\",\"3289\":\"Q384\"}}");
+                "{\"qidMap\":{\"1234\":\"Q345\",\"3289\":\"Q384\"},\"nameMap\":{\"1234\":\"new uni\",\"3289\":\"University of Ljubljana\"}}");
     }

     private void isMatchedTo(String qid, Cell cell) {
diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/ReconEntityRewriterTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/ReconEntityRewriterTest.java
index cfbaae0d3..5808289f7 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/ReconEntityRewriterTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/editing/ReconEntityRewriterTest.java
@@ -61,6 +61,7 @@ public class ReconEntityRewriterTest {
     public void testSuccessfulRewrite() {
         rewriter = new ReconEntityRewriter(library, TestingData.newIdA);
         library.setId(4567L, "Q1234");
+        library.setName(4567L, "Q1234");
         assertEquals(newlyCreated, rewriter.copy(TestingData.newIdB));
     }

@@ -75,6 +76,7 @@ public class ReconEntityRewriterTest {
     public void testSubjectRewritten() {
         ItemIdValue subject = TestingData.newIdB;
         library.setId(4567L, "Q1234");
+        library.setName(4567L, "Q1234");
         rewriter = new ReconEntityRewriter(library, subject);
         assertEquals(newlyCreated, rewriter.copy(subject));
     }
@@ -90,6 +92,7 @@ public class ReconEntityRewriterTest {
         ItemIdValue subject = TestingData.newIdA;
         rewriter = new ReconEntityRewriter(library, subject);
         library.setId(4567L, "Q1234");
+        library.setName(4567L, "label");
         TermedStatementEntityEdit update = new ItemEditBuilder(subject)
                 .addStatement(TestingData.generateStatementAddition(subject, TestingData.newIdB))
                 .addStatement(TestingData.generateStatementDeletion(subject, TestingData.existingId))
@@ -115,6 +118,7 @@ public class ReconEntityRewriterTest {
         ItemIdValue subject = TestingData.newIdA;
         rewriter = new ReconEntityRewriter(library, subject);
         library.setId(4567L, "Q1234");
+        library.setName(4567L, "Q1234");
         TermedStatementEntityEdit update = new ItemEditBuilder(TestingData.newIdB)
                 .addDescription(Datamodel.makeMonolingualTextValue("beschreibung", "de"), false)
                 .addAlias(Datamodel.makeMonolingualTextValue("darstellung", "de"))
@@ -134,6 +138,7 @@ public class ReconEntityRewriterTest {
         ItemIdValue subject = TestingData.matchedId;
         rewriter = new ReconEntityRewriter(library, subject);
         library.setId(4567L, "Q1234");
+        library.setName(4567L, "label");
         TermedStatementEntityEdit update = new ItemEditBuilder(subject)
                 .addStatement(TestingData.generateStatementAddition(subject, TestingData.newIdB))
                 .addStatement(TestingData.generateStatementDeletion(subject, TestingData.existingId))
@@ -159,6 +164,7 @@ public class ReconEntityRewriterTest {
         ItemIdValue subject = TestingData.existingId;
         rewriter = new ReconEntityRewriter(library, subject);
         library.setId(7654L, "P1234");
+        library.setName(7654L, "label");
         TermedStatementEntityEdit update = new ItemEditBuilder(subject)
                 .addStatement(TestingData.generateStatementAddition(subject, TestingData.newPropertyIdB))
                 .addStatement(TestingData.generateStatementDeletion(subject, TestingData.existingPropertyId))
diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperationTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperationTest.java
index 5f35fd64a..83f1e59fc 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperationTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/operations/PerformWikibaseEditsOperationTest.java
@@ -95,4 +95,25 @@ public class PerformWikibaseEditsOperationTest extends OperationTest {
         assertEquals(changeString, saveChange(change));
     }

+    @Test
+    public void testLoadChange_v2()
+            throws Exception {
+        String changeString = "newItems={\"qidMap\":{\"1234\":\"Q789\"},\"nameMap\":{\"1234\":\"Non-existent entity\"}}\n" + "/ec/\n";
+        LineNumberReader reader = makeReader(changeString);
+        Change change = PerformWikibaseEditsOperation.PerformWikibaseEditsChange.load(reader, pool);
+
+        project.rows.get(0).cells.set(0, TestingData.makeNewItemCell(1234L, "my new item"));
+
+        change.apply(project);
+
+        assertEquals(Recon.Judgment.Matched, project.rows.get(0).cells.get(0).recon.judgment);
+        assertEquals("Q789", project.rows.get(0).cells.get(0).recon.match.id);
+        assertEquals("Non-existent entity", project.rows.get(0).cells.get(0).recon.match.name);
+
+        change.revert(project);
+
+        assertEquals(Recon.Judgment.New, project.rows.get(0).cells.get(0).recon.judgment);
+
+        assertEquals(changeString, saveChange(change));
+    }
 }

Fixes #6996
Changes proposed in this pull request:
- Extended NewEntityLibrary to also hold label for new entities
- Updated EditBatchProcessor performEdit to set label from the value set in NewEntityLibrary
- Updated test cases

wikibase: Improve recon name for created entities