This is an automated email from the ASF dual-hosted git repository.

reschke pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 9a67005e784c007fcb4a5f99b565bcf928e26064
Author: Julian Reschke <[email protected]>
AuthorDate: Tue Oct 28 13:10:51 2025 +0100

    Reapply "OAK-11936: Allow updating the inference config via JMX (#2525)"
    
    This reverts commit acac0de477b4fe32e3a3268dd12bbef72e9267af.
---
 .../jackrabbit/oak/api/jmx/InferenceMBean.java     |   9 +
 .../elastic/query/inference/InferenceConfig.java   |  24 ++-
 .../query/inference/InferenceMBeanImpl.java        |  10 +-
 .../query/inference/InferenceConfigTest.java       |  71 +++++++
 .../jackrabbit/oak/json/JsonNodeBuilder.java       | 211 +++++++++++++++++++++
 .../jackrabbit/oak/json/JsonNodeBuilderTest.java   | 162 ++++++++++++++++
 6 files changed, 481 insertions(+), 6 deletions(-)

diff --git 
a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java 
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java
index 2690b6b64d..6f2187fe96 100644
--- 
a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java
+++ 
b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/InferenceMBean.java
@@ -37,4 +37,13 @@ public interface InferenceMBean {
      * Get the inference configuration as a Json string.
      */
     String getConfigNodeStateJson();
+
+    @Description("Adds or replaces the inference configuration at the 
specified path with the provided JSON. " +
+            "If saved successful, the system reInitializes with the updated 
configuration.")
+    void setConfigJson(@Name("path")
+                       @Description("The node path where the configuration 
should be stored.")
+                       String path,
+                       @Name("configJson")
+                       @Description("The inferenceConfig as a JSON sting.")
+                       String configJson);
 }
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
index 7d0ae473c2..886fe5a362 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfig.java
@@ -19,8 +19,10 @@
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
+import org.apache.jackrabbit.oak.json.JsonNodeBuilder;
 import org.apache.jackrabbit.oak.json.JsonUtils;
 import org.apache.jackrabbit.oak.plugins.index.IndexName;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
@@ -30,6 +32,7 @@ import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -242,6 +245,25 @@ public class InferenceConfig {
         }
     }
 
+    /**
+    * Updates the inference configuration with the provided JSON in the node 
store and reInitializes this class.
+    *
+    * @param path The node path where the configuration should be stored.
+    * @param jsonConfig The inferenceConfig as a JSON sting.
+    */
+    public static void updateAndReInitializeConfigJson(String path, String 
jsonConfig) {
+        lock.writeLock().lock();
+        try {
+            LOG.debug("Setting new InferenceConfig to path='{}' with 
content={}", path, jsonConfig);
+            JsonNodeBuilder.addOrReplace(INSTANCE.nodeStore, path, TYPE, 
jsonConfig);
+            InferenceConfig.reInitialize(INSTANCE.nodeStore, 
INSTANCE.statisticsProvider, path, INSTANCE.isInferenceEnabled, true);
+        } catch (CommitFailedException | IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
     private @NotNull Map<String, InferenceIndexConfig> getIndexConfigs() {
         lock.readLock().lock();
         try {
@@ -296,4 +318,4 @@ public class InferenceConfig {
         
builder.key(":enrich").encodedValue(enricherStatus.toString()).endObject();
         return JsopBuilder.prettyPrint(builder.toString());
     }
-} 
\ No newline at end of file
+}
\ No newline at end of file
diff --git 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
index bfd9f5f6fc..ef3f518356 100644
--- 
a/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
+++ 
b/oak-search-elastic/src/main/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceMBeanImpl.java
@@ -18,14 +18,9 @@
  */
 package org.apache.jackrabbit.oak.plugins.index.elastic.query.inference;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.jackrabbit.oak.api.jmx.InferenceMBean;
 import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean;
-import 
org.apache.jackrabbit.oak.plugins.index.elastic.ElasticIndexProviderService;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Objects;
 
 /**
  * An MBean that provides the inference configuration.
@@ -46,4 +41,9 @@ public class InferenceMBeanImpl extends 
AnnotatedStandardMBean implements Infere
     public String getConfigNodeStateJson() {
         return InferenceConfig.getInstance().getInferenceConfigNodeState();
     }
+
+    @Override
+    public void setConfigJson(String path, String configJson) {
+        InferenceConfig.updateAndReInitializeConfigJson(path, configJson);
+    }
 }
diff --git 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
index 0af97b2428..f7a789725e 100644
--- 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
+++ 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/query/inference/InferenceConfigTest.java
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import joptsimple.internal.Strings;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.json.JsonNodeBuilder;
 import 
org.apache.jackrabbit.oak.plugins.index.elastic.util.EnvironmentVariableProcessorUtil;
 import org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState;
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder;
@@ -37,6 +38,8 @@ import org.junit.Before;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.util.Map;
@@ -46,6 +49,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class InferenceConfigTest {
 
@@ -996,4 +1000,71 @@ public class InferenceConfigTest {
         InferenceModelConfig resultForNonExistentIndexName = 
inferenceConfig.getInferenceModelConfig("nonExistentIndex", defaultModelName);
         assertEquals("Non-existent index name should return NOOP", 
InferenceModelConfig.NOOP, resultForNonExistentIndexName);
     }
+
+    @Test
+    public void updateAndReInitializeCallsJsonNodeBuilder() {
+        String testPath = "/oak:index/testConfig";
+        String testJson = "{\"type\":\"inferenceConfig\",\"enabled\":true}";
+        InferenceConfig.reInitialize(nodeStore, testPath, true);
+
+        try (MockedStatic<JsonNodeBuilder> mockedStatic = 
Mockito.mockStatic(JsonNodeBuilder.class)) {
+            InferenceConfig.updateAndReInitializeConfigJson(testPath, 
testJson);
+            mockedStatic.verify(() -> JsonNodeBuilder.addOrReplace(
+                nodeStore,
+                testPath,
+                InferenceConfig.TYPE,
+                testJson
+            ), Mockito.times(1));
+        }
+    }
+
+    @Test
+    public void updateAndReInitializeDoesReInitialize() throws Exception {
+        NodeBuilder inferenceConfigBuilder = createNodePath(rootBuilder, 
DEFAULT_CONFIG_PATH);
+        inferenceConfigBuilder.setProperty(InferenceConstants.ENABLED, false);
+        nodeStore.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+
+        InferenceConfig.reInitialize(nodeStore, DEFAULT_CONFIG_PATH, true);
+        InferenceConfig initialConfig = InferenceConfig.getInstance();
+        assertFalse(initialConfig.isEnabled());
+
+        String enabledJson = "{\"type\":\"inferenceConfig\",\"enabled\":true}";
+        InferenceConfig.updateAndReInitializeConfigJson(DEFAULT_CONFIG_PATH, 
enabledJson);
+        InferenceConfig updatedConfig = InferenceConfig.getInstance();
+        assertTrue(updatedConfig.isEnabled());
+    }
+
+    @Test
+    public void updateAndReInitializeHandlesCommitFailedException() {
+        InferenceConfig.reInitialize(nodeStore, "/test", true);
+        try (MockedStatic<JsonNodeBuilder> mockedStatic = 
Mockito.mockStatic(JsonNodeBuilder.class)) {
+            mockedStatic.when(() -> JsonNodeBuilder.addOrReplace(
+                Mockito.any(NodeStore.class), Mockito.anyString(), 
Mockito.anyString(), Mockito.anyString()
+            )).thenThrow(new CommitFailedException("TEST", 0, "Test 
exception"));
+
+            try {
+                InferenceConfig.updateAndReInitializeConfigJson("/test", "{}");
+                fail("Expected RuntimeException to be thrown");
+            } catch (RuntimeException e) {
+                assertTrue(e.getCause() instanceof CommitFailedException);
+            }
+        }
+    }
+
+    @Test
+    public void updateAndReInitializeHandlesIOException() {
+        InferenceConfig.reInitialize(nodeStore, "/test", true);
+        try (MockedStatic<JsonNodeBuilder> mockedStatic = 
Mockito.mockStatic(JsonNodeBuilder.class)) {
+            mockedStatic.when(() -> JsonNodeBuilder.addOrReplace(
+                Mockito.any(NodeStore.class), Mockito.anyString(), 
Mockito.anyString(), Mockito.anyString()
+            )).thenThrow(new IOException("Test IO exception"));
+
+            try {
+                InferenceConfig.updateAndReInitializeConfigJson("/test", "{}");
+                fail("Expected RuntimeException to be thrown");
+            } catch (RuntimeException e) {
+                assertTrue(e.getCause() instanceof IOException);
+            }
+        }
+    }
 } 
\ No newline at end of file
diff --git 
a/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonNodeBuilder.java
 
b/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonNodeBuilder.java
new file mode 100644
index 0000000000..2665ef5bc7
--- /dev/null
+++ 
b/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonNodeBuilder.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.json;
+
+import java.util.Map.Entry;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.UUID;
+
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.commons.json.JsopReader;
+import org.apache.jackrabbit.oak.commons.json.JsopTokenizer;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A utility class to persist a configuration that is in the form of JSON into
+ * the node store.
+ * <p>
+ * This is used to persist a small set of configuration nodes, eg. index
+ * definitions, using a simple JSON format.
+ * <p>
+ * The node type does not need to be set on a per-node basis. Where it is
+ * missing, the provided node type is used (e.g. "nt:unstructured")
+ * <p>
+ * A "jcr:uuid" is automatically added for nodes of type "nt:resource".
+ * <p>
+ * String, string arrays, boolean, blob, long, and double values are supported.
+ * Values that start with ":blobId:...base64..." are stored as binaries. 
"str:",
+ * "nam:" and "dat:" prefixes are removed.
+ * <p>
+ * "null" entries are not supported.
+ */
+public class JsonNodeBuilder {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(JsonNodeBuilder.class);
+
+    /**
+     * Add or replace a node in the node store, including all child nodes.
+     *
+     * @param nodeStore  the target node store
+     * @param targetPath the target path where the node(s) is/are replaced
+     * @param nodeType   the node type of the new node (eg. "nt:unstructured")
+     * @param jsonString the json string with the node data
+     * @throws CommitFailedException if storing the nodes failed
+     * @throws IOException           if storing a blob failed
+     */
+    public static void addOrReplace(NodeStore nodeStore, String targetPath, 
String nodeType, String jsonString) throws CommitFailedException, IOException {
+        LOG.info("Storing {}: {}", targetPath, jsonString);
+        JsonObject json = JsonObject.fromJson(jsonString, true);
+        NodeBuilder root = nodeStore.getRoot().builder();
+        NodeBuilder builder = root;
+        for (String name : PathUtils.elements(targetPath)) {
+            NodeBuilder child = builder.child(name);
+            if (!child.hasProperty("jcr:primaryType")) {
+                if (nodeType.indexOf("/") >= 0) {
+                    throw new IllegalStateException("Illegal node type: " + 
nodeType);
+                }
+                child.setProperty("jcr:primaryType", nodeType, Type.NAME);
+            }
+            builder = child;
+        }
+        storeConfigNode(nodeStore, builder, nodeType, json);
+        nodeStore.merge(root, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+    }
+
+    private static void storeConfigNode(NodeStore nodeStore, NodeBuilder 
builder, String nodeType, JsonObject json) throws IOException {
+        for (Entry<String, JsonObject> e : json.getChildren().entrySet()) {
+            String k = e.getKey();
+            JsonObject v = e.getValue();
+            storeConfigNode(nodeStore, builder.child(k), nodeType, v);
+        }
+        for (String child : builder.getChildNodeNames()) {
+            if (!json.getChildren().containsKey(child)) {
+                builder.child(child).remove();
+            }
+        }
+        for (Entry<String, String> e : json.getProperties().entrySet()) {
+            String k = e.getKey();
+            String v = e.getValue();
+            storeConfigProperty(nodeStore, builder, k, v);
+        }
+        if (!json.getProperties().containsKey("jcr:primaryType")) {
+            builder.setProperty("jcr:primaryType", nodeType, Type.NAME);
+        }
+        for (PropertyState prop : builder.getProperties()) {
+            if ("jcr:primaryType".equals(prop.getName())) {
+                continue;
+            }
+            if (!json.getProperties().containsKey(prop.getName())) {
+                builder.removeProperty(prop.getName());
+            }
+        }
+        if ("nt:resource".equals(JsonNodeBuilder.oakStringValue(json, 
"jcr:primaryType"))) {
+            if (!json.getProperties().containsKey("jcr:uuid")) {
+                String uuid = UUID.randomUUID().toString();
+                builder.setProperty("jcr:uuid", uuid);
+            }
+        }
+    }
+
+    private static void storeConfigProperty(NodeStore nodeStore, NodeBuilder 
builder, String propertyName, String value) throws IOException {
+        if (value.startsWith("\"")) {
+            // string or blob
+            value = JsopTokenizer.decodeQuoted(value);
+            if (value.startsWith(":blobId:")) {
+                String base64 = value.substring(":blobId:".length());
+                byte[] bytes = 
Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8));
+                Blob blob;
+                blob = nodeStore.createBlob(new ByteArrayInputStream(bytes));
+                builder.setProperty(propertyName, blob);
+            } else {
+                if ("jcr:primaryType".equals(propertyName)) {
+                    builder.setProperty(propertyName, value, Type.NAME);
+                } else {
+                    builder.setProperty(propertyName, value);
+                }
+            }
+        } else if (value.equals("null")) {
+            throw new IllegalArgumentException("Removing entries is not 
supported");
+        } else if (value.equals("true")) {
+            builder.setProperty(propertyName, true);
+        } else if (value.equals("false")) {
+            builder.setProperty(propertyName, false);
+        } else if (value.startsWith("[")) {
+            JsopTokenizer tokenizer = new JsopTokenizer(value);
+            ArrayList<String> result = new ArrayList<>();
+            tokenizer.matches('[');
+            if (!tokenizer.matches(']')) {
+                do {
+                    if (!tokenizer.matches(JsopReader.STRING)) {
+                        throw new IllegalArgumentException("Could not process 
string array " + value);
+                    }
+                    result.add(tokenizer.getEscapedToken());
+                } while (tokenizer.matches(','));
+                tokenizer.read(']');
+            }
+            tokenizer.read(JsopReader.END);
+            builder.setProperty(propertyName, result, Type.STRINGS);
+        } else if (value.indexOf('.') >= 0 || value.toLowerCase().indexOf("e") 
>= 0) {
+            // double
+            try {
+                Double d = Double.parseDouble(value);
+                builder.setProperty(propertyName, d);
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Could not parse double " + 
value);
+            }
+        } else if (value.startsWith("-") || (!value.isEmpty() && 
Character.isDigit(value.charAt(0)))) {
+            // long
+            try {
+                Long x = Long.parseLong(value);
+                builder.setProperty(propertyName, x);
+            } catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Could not parse long " + 
value);
+            }
+        } else {
+            throw new IllegalArgumentException("Unsupported value " + value);
+        }
+    }
+
+    static String oakStringValue(JsonObject json, String propertyName) {
+        String value = json.getProperties().get(propertyName);
+        if (value == null) {
+            return null;
+        }
+        return oakStringValue(value);
+    }
+
+    static String oakStringValue(String value) {
+        if (!value.startsWith("\"")) {
+            // support numbers
+            return value;
+        }
+        value = JsopTokenizer.decodeQuoted(value);
+        if (value.startsWith(":blobId:")) {
+            value = value.substring(":blobId:".length());
+            value = new 
String(Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)), 
StandardCharsets.UTF_8);
+        } else if (value.startsWith("str:") || value.startsWith("nam:") || 
value.startsWith("dat:")) {
+            value = value.substring("str:".length());
+        }
+        return value;
+    }
+}
diff --git 
a/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonNodeBuilderTest.java
 
b/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonNodeBuilderTest.java
new file mode 100644
index 0000000000..5abc7094ef
--- /dev/null
+++ 
b/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonNodeBuilderTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.json;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import java.io.IOException;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.junit.Before;
+import org.junit.Test;
+
+public class JsonNodeBuilderTest {
+
+    private MemoryNodeStore ns;
+
+    @Before
+    public void before() {
+        ns = new MemoryNodeStore();
+    }
+
+    @Test
+    public void addNodeTypeAndUUID() throws CommitFailedException, IOException 
{
+        JsonObject json = JsonObject.fromJson("{\n"
+                + "  \"includedPaths\": \"/same\",\n"
+                + "  \"jcr:primaryType\": \"nt:unstructured\",\n"
+                + "  \"queryPaths\": \"/same\",\n"
+                + "  \"type\": \"lucene\",\n"
+                + "  \"diff.json\": {\n"
+                + "    \"jcr:primaryType\": \"nt:file\",\n"
+                + "    \"jcr:content\": {\n"
+                + "      \"jcr:data\": \":blobId:dGVzdA==\",\n"
+                + "      \"jcr:mimeType\": \"application/json\",\n"
+                + "      \"jcr:primaryType\": \"nt:resource\"\n"
+                + "    }\n"
+                + "  }\n"
+                + "}" , true);
+        JsonNodeBuilder.addOrReplace(ns, "/test", "nt:test", json.toString());
+        String json2 = JsonUtils.nodeStateToJson(ns.getRoot(), 5);
+        json2 = json2.replaceAll("jcr:uuid\" : \".*\"", "jcr:uuid\" : 
\"...\"");
+        assertEquals("{\n"
+                + "  \"test\" : {\n"
+                + "    \"queryPaths\" : \"/same\",\n"
+                + "    \"includedPaths\" : \"/same\",\n"
+                + "    \"jcr:primaryType\" : \"nt:unstructured\",\n"
+                + "    \"type\" : \"lucene\",\n"
+                + "    \"diff.json\" : {\n"
+                + "      \"jcr:primaryType\" : \"nt:file\",\n"
+                + "      \"jcr:content\" : {\n"
+                + "        \"jcr:mimeType\" : \"application/json\",\n"
+                + "        \"jcr:data\" : \"test\",\n"
+                + "        \"jcr:primaryType\" : \"nt:resource\",\n"
+                + "        \"jcr:uuid\" : \"...\"\n"
+                + "      }\n"
+                + "    }\n"
+                + "  }\n"
+                + "}", json2);
+
+        json = JsonObject.fromJson(
+                "{\"number\":1," +
+                        "\"double2\":1.0," +
+                        "\"child2\":{\"y\":2}}", true);
+        JsonNodeBuilder.addOrReplace(ns, "/test", "nt:test", json.toString());
+        assertEquals("{\n"
+                + "  \"test\" : {\n"
+                + "    \"number\" : 1,\n"
+                + "    \"double2\" : 1.0,\n"
+                + "    \"jcr:primaryType\" : \"nt:test\",\n"
+                + "    \"child2\" : {\n"
+                + "      \"y\" : 2,\n"
+                + "      \"jcr:primaryType\" : \"nt:test\"\n"
+                + "    }\n"
+                + "  }\n"
+                + "}", JsonUtils.nodeStateToJson(ns.getRoot(), 5));
+    }
+
+    @Test
+    public void storeDifferentDataTypes() throws CommitFailedException, 
IOException {
+        JsonObject json = JsonObject.fromJson(
+                "{\"number\":1," +
+                        "\"double\":1.0," +
+                        "\"string\":\"hello\"," +
+                        "\"array\":[\"a\",\"b\"]," +
+                        "\"child\":{\"x\":1}," +
+                        "\"blob\":\":blobId:dGVzdA==\"}", true);
+        JsonNodeBuilder.addOrReplace(ns, "/test", "nt:test", json.toString());
+        assertEquals("{\n"
+                + "  \"test\" : {\n"
+                + "    \"number\" : 1,\n"
+                + "    \"blob\" : \"test\",\n"
+                + "    \"string\" : \"hello\",\n"
+                + "    \"array\" : [ \"a\", \"b\" ],\n"
+                + "    \"double\" : 1.0,\n"
+                + "    \"jcr:primaryType\" : \"nt:test\",\n"
+                + "    \"child\" : {\n"
+                + "      \"x\" : 1,\n"
+                + "      \"jcr:primaryType\" : \"nt:test\"\n"
+                + "    }\n"
+                + "  }\n"
+                + "}", JsonUtils.nodeStateToJson(ns.getRoot(), 5));
+
+        json = JsonObject.fromJson(
+                "{\"number\":1," +
+                        "\"boolTrue\":true," +
+                        "\"boolFalse\":false," +
+                        "\"double2\":1.0," +
+                        "\"child2\":{\"y\":2}}", true);
+        JsonNodeBuilder.addOrReplace(ns, "/test", "nt:test", json.toString());
+        assertEquals("{\n"
+                + "  \"test\" : {\n"
+                + "    \"number\" : 1,\n"
+                + "    \"boolTrue\" : true,\n"
+                + "    \"boolFalse\" : false,\n"
+                + "    \"double2\" : 1.0,\n"
+                + "    \"jcr:primaryType\" : \"nt:test\",\n"
+                + "    \"child2\" : {\n"
+                + "      \"y\" : 2,\n"
+                + "      \"jcr:primaryType\" : \"nt:test\"\n"
+                + "    }\n"
+                + "  }\n"
+                + "}", JsonUtils.nodeStateToJson(ns.getRoot(), 5));
+    }
+
+    @Test
+    public void illegalNodeTypesAreProhibited() {
+        String simpleJson = "{\"property\": \"value\"}";
+        
+        IllegalStateException exception = assertThrows(
+            IllegalStateException.class,
+            () -> JsonNodeBuilder.addOrReplace(ns, "/test", 
"invalid/nodetype", simpleJson)
+        );
+        assertEquals("Illegal node type: invalid/nodetype", 
exception.getMessage());
+    }
+
+    @Test
+    public void removingEntriesIsProhibited() {
+        String jsonWithNull = "{\"nullProperty\": null}";
+        
+        IllegalArgumentException exception = assertThrows(
+            IllegalArgumentException.class,
+            () -> JsonNodeBuilder.addOrReplace(ns, "/test", "nt:unstructured", 
jsonWithNull)
+        );
+        assertEquals("Removing entries is not supported", 
exception.getMessage());
+    }
+}

Reply via email to