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()); + } +}
