This is an automated email from the ASF dual-hosted git repository.
thomasm pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new 174dce112b OAK-11936: Allow updating the inference config via JMX
(#2525)
174dce112b is described below
commit 174dce112b33c0f3e59f665f96ab3ed390397506
Author: Marvin <[email protected]>
AuthorDate: Mon Oct 20 14:36:24 2025 +0200
OAK-11936: Allow updating the inference config via JMX (#2525)
* OAK-11936: Allow updating the inference config via JMX
* OAK-11936: Apache License added to new classes
---------
Co-authored-by: marvinw <[email protected]>
---
.../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());
+ }
+}