Repository: OpenRefine/OpenRefine (java)

Issue:
Error when editing CustomMetadata field 
When trying to edit the customMetadata field in the about section of a project to add new values, the OpenRefine server crashes and throws a ``` java.lang.IllegalArgumentException: Can not set java.util.Map field com.google.refine.ProjectMetadata._customMetadata to java.lang.String```. The issue is the same as well when interacting with the OpenRefine API through ```set-project-metadata``` endpoint as well.

### To Reproduce
Steps to reproduce the behavior:
1. First, open the Open Project section to the left of the screen
2. Then, choose any project (with empty customMetadata) and press on about.
3. Finally, next to the customMetada field, press the edit button and add any JSON-like format.



### Current Results
The newly added values in the customMetadata field is not saved when the page is reloaded and the server crashes (error shown below).  

### Expected Behavior
The newly added values in the customMetadata field should be saved when the page is reloaded.

### Screenshots
![image](https://user-images.githubusercontent.com/51036803/230028568-53b7d757-f3b2-4508-b50a-fd341626a0f0.png)

### Versions<!--  (please complete the following information)-->
 - Operating System: Windows 10
 - Browser Version: Chrome Version 111.0.5563.147
 - JRE or JDK Version: openjdk 1.8.0_345 <!--  output of "java -version" e.g. JRE 1.8.0_181 -->
 - OpenRefine: 3.7


Pull request:
fix custom metadata edit action
Fixes #5772 

Changes proposed in this pull request:
- edit setAnyField() method in ProjectMetadata class
- add case to edit customMetadata field
- parse given String to JSON and JSON to HashMap


Patch:
diff --git a/README.md b/README.md
index 570405b06..a31129741 100644
--- a/README.md
+++ b/README.md
@@ -20,11 +20,8 @@ Community forum: **https://forum.openrefine.org**
 
 ## Snapshot releases
 
-Latest development version, packaged for:
-* [Linux](https://oss.sonatype.org/service/local/artifact/maven/content?r=snapshots&g=org.openrefine&a=openrefine&v=3.8-SNAPSHOT&c=linux&p=tar.gz)
-* [MacOS](https://oss.sonatype.org/service/local/artifact/maven/content?r=snapshots&g=org.openrefine&a=openrefine&v=3.8-SNAPSHOT&c=mac&p=dmg)
-* [Windows without embedded JRE](https://oss.sonatype.org/service/local/artifact/maven/content?r=snapshots&g=org.openrefine&a=openrefine&v=3.8-SNAPSHOT&c=win&p=zip)
-* [Windows with embedded JRE](https://oss.sonatype.org/service/local/artifact/maven/content?r=snapshots&g=org.openrefine&a=openrefine&v=3.8-SNAPSHOT&c=win-with-java&p=zip)
+You can download snapshots of the development version of OpenRefine.
+To do so, you need to be logged in to GitHub. Then, click on the first item with a green tick / check mark on [this page](https://github.com/OpenRefine/OpenRefine/actions/workflows/snapshot_release.yml) and scroll down to the Artifacts section to find the version that matches your operating system.
 
 ## Run from source
 
diff --git a/main/src/com/google/refine/ProjectMetadata.java b/main/src/com/google/refine/ProjectMetadata.java
index 50cef88de..b90e18228 100644
--- a/main/src/com/google/refine/ProjectMetadata.java
+++ b/main/src/com/google/refine/ProjectMetadata.java
@@ -49,6 +49,8 @@ import org.slf4j.LoggerFactory;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.json.JsonReadFeature;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonView;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -374,9 +376,17 @@ public class ProjectMetadata {
             Field metaField = metaClass.getDeclaredField("_" + metaName);
             if (metaName.equals("tags")) {
                 metaField.set(this, valueString.split(","));
+            } else if (metaName.equals("customMetadata")) {
+                ParsingUtilities.mapper.enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature());
+                Map<String, Object> map = ParsingUtilities.mapper.readValue(valueString, HashMap.class);
+                metaField.set(this, map);
             } else {
                 metaField.set(this, valueString);
             }
+        } catch (JsonProcessingException e) {
+            String errorMessage = "Error reading JSON: " + e.getOriginalMessage();
+            logger.error(errorMessage);
+            throw new RuntimeException(errorMessage);
         } catch (NoSuchFieldException e) {
             updateUserMetadata(metaName, valueString);
         } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
diff --git a/packaging/pom.xml b/packaging/pom.xml
index 99dfd5890..a07206e70 100644
--- a/packaging/pom.xml
+++ b/packaging/pom.xml
@@ -513,88 +513,6 @@
           <skip>true</skip>
         </configuration>
       </plugin>
-
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>build-helper-maven-plugin</artifactId>
-        <version>${build-helper-maven-plugin.version}</version>
-        <inherited>false</inherited>
-        <executions>
-          <!-- Attach tar.gz artifact for linux -->
-          <execution>
-              <id>attach-linux</id>
-              <phase>deploy</phase>
-              <goals>
-                  <goal>attach-artifact</goal>
-              </goals>
-              <configuration>
-                  <artifacts>
-                      <artifact>
-                          <file>${project.build.directory}/${project.build.finalName}-linux-${project.version}.tar.gz</file>
-                          <type>tar.gz</type>
-                          <classifier>linux</classifier>
-                      </artifact>
-                  </artifacts>
-              </configuration>
-          </execution>
-
-          <!-- Attach the zip file for windows, without embedded JRE -->
-          <execution>
-              <id>attach-win-without-jre</id>
-              <phase>deploy</phase>
-              <goals>
-                  <goal>attach-artifact</goal>
-              </goals>
-              <configuration>
-                  <skipAttach>${skipCreateExe}</skipAttach>
-                  <artifacts>
-                      <artifact>
-                          <file>${project.build.directory}/${project.build.finalName}-win-${project.version}.zip</file>
-                          <type>zip</type>
-                          <classifier>win</classifier>
-                      </artifact>
-                  </artifacts>
-              </configuration>
-          </execution>
-
-          <!-- Attach the zip file for windows, with embedded JRE -->
-          <execution>
-              <id>attach-win-with-jre</id>
-              <phase>deploy</phase>
-              <goals>
-                  <goal>attach-artifact</goal>
-              </goals>
-              <configuration>
-                  <skipAttach>${skipCreateExe}</skipAttach>
-                  <artifacts>
-                      <artifact>
-                          <file>${project.build.directory}/${project.build.finalName}-win-with-java-${project.version}.zip</file>
-                          <type>zip</type>
-                          <classifier>win-with-java</classifier>
-                      </artifact>
-                  </artifacts>
-              </configuration>
-          </execution>
-
-          <!-- Attach the final DMG for MacOS -->
-          <execution>
-              <id>attach-dmg</id>
-              <phase>deploy</phase>
-              <goals>
-                  <goal>attach-artifact</goal>
-              </goals>
-              <configuration>
-                  <artifacts>
-                      <artifact>
-                          <file>${project.build.directory}/${project.build.finalName}-mac-${project.version}.dmg</file>
-                          <type>dmg</type>
-                          <classifier>mac</classifier>
-                      </artifact>
-                  </artifacts>
-              </configuration>
-          </execution>
-        </executions>
-      </plugin>
     </plugins>
     <pluginManagement>
     	<plugins>


Test patch:
diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index 739d96726..c9b25e828 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.44.0",
+        "eslint": "^8.45.0",
         "eslint-config-google": "^0.14.0",
         "eslint-config-prettier": "^8.8.0",
         "eslint-plugin-cypress": "^2.13.3",
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 8bf8be48c..066e8d958 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -594,10 +594,10 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^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@^8.44.0:
-  version "8.44.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500"
-  integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==
+eslint@^8.45.0:
+  version "8.45.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.45.0.tgz#bab660f90d18e1364352c0a6b7c6db8edb458b78"
+  integrity sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.4.0"
@@ -624,7 +624,6 @@ eslint@^8.44.0:
     globals "^13.19.0"
     graphemer "^1.4.0"
     ignore "^5.2.0"
-    import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
     is-path-inside "^3.0.3"
@@ -636,7 +635,6 @@ eslint@^8.44.0:
     natural-compare "^1.4.0"
     optionator "^0.9.3"
     strip-ansi "^6.0.1"
-    strip-json-comments "^3.1.0"
     text-table "^0.2.0"
 
 espree@^9.6.0:
@@ -948,7 +946,7 @@ ignore@^5.2.0:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
   integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
 
-import-fresh@^3.0.0, import-fresh@^3.2.1:
+import-fresh@^3.2.1:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz"
   integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -1511,7 +1509,7 @@ strip-final-newline@^2.0.0:
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+strip-json-comments@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
diff --git a/main/tests/server/src/com/google/refine/commands/project/SetProjectMetadataCommandTests.java b/main/tests/server/src/com/google/refine/commands/project/SetProjectMetadataCommandTests.java
index d80f3b8fb..bbee28a84 100644
--- a/main/tests/server/src/com/google/refine/commands/project/SetProjectMetadataCommandTests.java
+++ b/main/tests/server/src/com/google/refine/commands/project/SetProjectMetadataCommandTests.java
@@ -41,6 +41,8 @@ import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -211,4 +213,42 @@ public class SetProjectMetadataCommandTests extends RefineTest {
         verify(request, times(2)).getParameter("project");
         verify(projMan, times(1)).getProject(PROJECT_ID_LONG);
     }
+
+    @Test
+    public void setCustomMetadataTest() {
+        String customMetadata = this.getValidCustomMetadata();
+        ProjectMetadata meta = proj.getMetadata();
+        meta.setAnyField("customMetadata", customMetadata);
+
+        Assert.assertEquals(meta.getCustomMetadata("name"), "test");
+        Assert.assertEquals(meta.getCustomMetadata("isValid"), true);
+        Assert.assertEquals(meta.getCustomMetadata("id"), 1);
+        Assert.assertTrue(meta.getCustomMetadata("companies") instanceof List);
+        Assert.assertTrue(meta.getCustomMetadata("information") instanceof Map);
+    }
+
+    @Test
+    public void setInvalidCustomMetadataTest() {
+        String customMetadata = this.getInvalidCustomMetadata();
+        ProjectMetadata meta = proj.getMetadata();
+
+        Assert.assertThrows(() -> meta.setAnyField("customMetadata", customMetadata));
+    }
+
+    private String getValidCustomMetadata() {
+        return "{\n" + //
+                "  \"name\":\"test\",\n" + //
+                "  \"companies\":[\"Google\", \"IBM\", \"meta\"],\n" + //
+                "  \"isValid\": true,\n" + //
+                "  \"information\": {\n" + //
+                "    \"text-field\":\"value\",\n" + //
+                "    \"numeric-field\": 1\n" + //
+                "  },\n" + //
+                "  \"id\":1\n" + //
+                "}";
+    }
+
+    private String getInvalidCustomMetadata() {
+        return "{\n  \"name\":test\n}";
+    }
 }