This is an automated email from the ASF dual-hosted git repository. mkataria pushed a commit to branch OAK-11693 in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
commit 5f64fe20d65e369202c673a8c75d40adaf190cf9 Author: Mohit Kataria <[email protected]> AuthorDate: Mon May 5 13:51:50 2025 +0530 OAK-11693: Parse inference query and extract out inference config and text query --- .../oak/api/jmx/QueryEngineSettingsMBean.java | 14 + .../jackrabbit/oak/api/jmx/package-info.java | 2 +- .../main/java/org/apache/jackrabbit/oak/Oak.java | 11 + .../jackrabbit/oak/query/QueryEngineSettings.java | 19 +- .../oak/query/QueryEngineSettingsService.java | 16 ++ .../oak/query/ast/FullTextSearchImpl.java | 49 ++-- .../jackrabbit/oak/query/index/FilterImpl.java | 5 + .../org/apache/jackrabbit/oak/osgi/OSGiIT.java | 5 + oak-query-spi/pom.xml | 9 + .../apache/jackrabbit/oak/spi/query/Filter.java | 8 + .../jackrabbit/oak/spi/query/QueryLimits.java | 4 + .../oak/spi/query/fulltext/InferenceQuery.java | 100 ++++++++ .../spi/query/fulltext/InferenceQueryConfig.java | 54 ++++ .../oak/spi/query/fulltext/package-info.java | 2 +- .../jackrabbit/oak/spi/query/package-info.java | 2 +- .../query/fulltext/InferenceQueryConfigTest.java | 53 ++++ .../oak/spi/query/fulltext/InferenceQueryTest.java | 92 +++++++ oak-store-spi/pom.xml | 5 + .../org/apache/jackrabbit/oak/json/JsonUtils.java | 281 +++++++++++++++++++++ .../apache/jackrabbit/oak/json/JsonUtilsTest.java | 270 ++++++++++++++++++++ 20 files changed, 978 insertions(+), 23 deletions(-) diff --git a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java index df9a550029..93bfc9888c 100644 --- a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java +++ b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/QueryEngineSettingsMBean.java @@ -107,6 +107,20 @@ public interface QueryEngineSettingsMBean { */ void setFailTraversal(boolean failTraversal); + /** + * Get whether the query engine will parse the query and infers inference config + * + * @return true if inference is enabled + */ + boolean isInferenceEnabled(); + + /** + * Set whether to parse for inference index config to use from query. + * + * @param isInferenceEnabled the new value for this setting + */ + void setInferenceEnabled(boolean isInferenceEnabled); + /** * Whether the query result size should return an estimation for large queries. * diff --git a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java index 43d162d725..678163b257 100644 --- a/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java +++ b/oak-api/src/main/java/org/apache/jackrabbit/oak/api/jmx/package-info.java @@ -15,7 +15,7 @@ * limitations under the License. */ -@Version("4.13.0") +@Version("4.14.0") package org.apache.jackrabbit.oak.api.jmx; import org.osgi.annotation.versioning.Version; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java index b1eb93ec9b..7009a89399 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java @@ -411,6 +411,7 @@ public class Oak { this.queryEngineSettings.setLimitInMemory(settings.getLimitInMemory()); this.queryEngineSettings.setLimitReads(settings.getLimitReads()); this.queryEngineSettings.setStrictPathRestriction(settings.getStrictPathRestriction()); + this.queryEngineSettings.setInferenceEnabled(settings.isInferenceEnabled()); return this; } @@ -957,6 +958,16 @@ public class Oak { settings.setFailTraversal(failQueriesWithoutIndex); } + @Override + public boolean isInferenceEnabled() { + return settings.isInferenceEnabled(); + } + + @Override + public void setInferenceEnabled(boolean isInferenceEnabled) { + settings.setInferenceEnabled(isInferenceEnabled); + } + @Override public boolean isFastQuerySize() { return settings.isFastQuerySize(); diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java index bb90eb2ad9..9527ddb927 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java @@ -75,7 +75,12 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit private static final boolean DEFAULT_FULL_TEXT_COMPARISON_WITHOUT_INDEX = Boolean.getBoolean("oak.queryFullTextComparisonWithoutIndex"); - + + public static final String OAK_INFERENCE_ENABLED = "oak.query.InferenceEnabled"; + private static final boolean DEFAULT_INFERENCE_ENABLED = + Boolean.getBoolean(OAK_INFERENCE_ENABLED); + + private long limitInMemory = DEFAULT_QUERY_LIMIT_IN_MEMORY; private long limitReads = DEFAULT_QUERY_LIMIT_READS; @@ -86,6 +91,8 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit private boolean fullTextComparisonWithoutIndex = DEFAULT_FULL_TEXT_COMPARISON_WITHOUT_INDEX; + + private boolean isInferenceEnabled = DEFAULT_INFERENCE_ENABLED; private boolean sql2Optimisation = Boolean.parseBoolean(System.getProperty(SQL2_OPTIMISATION_FLAG, "true")); @@ -200,6 +207,16 @@ public class QueryEngineSettings implements QueryEngineSettingsMBean, QueryLimit this.failTraversal = failTraversal; } + @Override + public boolean isInferenceEnabled() { + return this.isInferenceEnabled; + } + + @Override + public void setInferenceEnabled(boolean isInferenceEnabled) { + this.isInferenceEnabled = isInferenceEnabled; + } + @Override public boolean isFastQuerySize() { return fastQuerySize; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java index f52542a991..f30b3d5c3e 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettingsService.java @@ -86,6 +86,12 @@ public class QueryEngineSettingsService { ) String[] ignoredClassNamesInCallTrace() default {}; + @AttributeDefinition( + name = "Enable inference query parsing", + description = "If enabled the query engine will parse the query, infers the inference index config from query" + ) + boolean isInferenceEnabled() default DEFAULT_IS_INFERENCE_ENABLED; + } // should be the same as QueryEngineSettings.DEFAULT_QUERY_LIMIT_IN_MEMORY @@ -102,6 +108,9 @@ public class QueryEngineSettingsService { static final String QUERY_FAST_QUERY_SIZE = "fastQuerySize"; static final String DISABLED_STRICT_PATH_RESTRICTION = "DISABLE"; + private static final boolean DEFAULT_IS_INFERENCE_ENABLED = false; + static final String INFERENCE_ENABLED = "inferenceEnabled"; + private final Logger log = LoggerFactory.getLogger(getClass()); @Reference @@ -130,6 +139,13 @@ public class QueryEngineSettingsService { logMsg(QUERY_FAIL_TRAVERSAL, QueryEngineSettings.OAK_QUERY_FAIL_TRAVERSAL); } + if (System.getProperty(QueryEngineSettings.OAK_INFERENCE_ENABLED) == null) { + boolean isInferenceEnabled = config.isInferenceEnabled(); + queryEngineSettings.setInferenceEnabled(isInferenceEnabled); + } else { + logMsg(INFERENCE_ENABLED, QueryEngineSettings.OAK_INFERENCE_ENABLED); + } + queryEngineSettings.setIgnoredClassNamesInCallTrace(config.ignoredClassNamesInCallTrace()); boolean fastQuerySizeSysProp = QueryEngineSettings.DEFAULT_FAST_QUERY_SIZE; diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java index 6b3fa3f4e9..a630ea32f9 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java @@ -18,24 +18,25 @@ */ package org.apache.jackrabbit.oak.query.ast; -import static org.apache.jackrabbit.oak.api.Type.STRING; -import static org.apache.jackrabbit.oak.api.Type.STRINGS; - -import java.text.ParseException; -import java.util.Collections; -import java.util.Set; - import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.PropertyValue; import org.apache.jackrabbit.oak.api.Tree; import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.commons.PathUtils; +import org.apache.jackrabbit.oak.plugins.memory.PropertyValues; +import org.apache.jackrabbit.oak.query.index.FilterImpl; +import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex; import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextContains; import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression; import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextParser; -import org.apache.jackrabbit.oak.query.index.FilterImpl; -import org.apache.jackrabbit.oak.plugins.memory.PropertyValues; -import org.apache.jackrabbit.oak.spi.query.QueryIndex.FulltextQueryIndex; +import org.apache.jackrabbit.oak.spi.query.fulltext.InferenceQuery; + +import java.text.ParseException; +import java.util.Collections; +import java.util.Set; + +import static org.apache.jackrabbit.oak.api.Type.STRING; +import static org.apache.jackrabbit.oak.api.Type.STRINGS; /** * A fulltext "contains(...)" condition. @@ -69,7 +70,7 @@ public class FullTextSearchImpl extends ConstraintImpl { } else { this.propertyName = propertyName; } - + this.fullTextSearchExpression = fullTextSearchExpression; } @@ -137,17 +138,27 @@ public class FullTextSearchImpl extends ConstraintImpl { } String p2 = normalizePropertyName(p); String rawText = getRawText(v); - FullTextExpression e = FullTextParser.parse(p2, rawText); + String queryText = rawText; + // To use inference we need to add inferenceconfig information in query. The format for the query is + // <inferenceprefix><json with inferenceModelConfig><inferencePrefix> + // e.g. ?{"inferenceModelConfig": "ada-test-model"}?little red fox + // So here we split the query into text part of query and inferenceConfig part of query. + // Afterwards we only parse text part of query as this part of query is what we want to search. + if (query.getSettings().isInferenceEnabled()) { + InferenceQuery inferenceQuery = new InferenceQuery(rawText); + queryText = inferenceQuery.getQueryText(); + } + FullTextExpression e = FullTextParser.parse(p2, queryText); return new FullTextContains(p2, rawText, e); } catch (ParseException e) { throw new IllegalArgumentException("Invalid expression: " + fullTextSearchExpression, e); } } - + String getRawText(PropertyValue v) { return v.getValue(Type.STRING); } - + @Override public Set<SelectorImpl> getSelectors() { return Collections.singleton(selector); @@ -155,9 +166,9 @@ public class FullTextSearchImpl extends ConstraintImpl { /** * verify that a property exists in the node. {@code property IS NOT NULL} - * + * * @param propertyName the property to check - * @param selector the selector to work with + * @param selector the selector to work with * @return true if the property is there, false otherwise. */ boolean enforcePropertyExistence(String propertyName, SelectorImpl selector) { @@ -167,7 +178,7 @@ public class FullTextSearchImpl extends ConstraintImpl { } return true; } - + @Override public boolean evaluate() { // disable evaluation if a fulltext index is used, @@ -225,7 +236,7 @@ public class FullTextSearchImpl extends ConstraintImpl { } return getFullTextConstraint(selector).evaluate(buff.toString()); } - + @Override public boolean evaluateStop() { // if a fulltext index is used, then we are fine @@ -278,7 +289,7 @@ public class FullTextSearchImpl extends ConstraintImpl { /** * restrict the provided property to the property to the provided filter achieving so * {@code property IS NOT NULL} - * + * * @param propertyName * @param f */ diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java index 6995c0e418..a0d41c1e6f 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/index/FilterImpl.java @@ -257,6 +257,11 @@ public class FilterImpl implements Filter { return alwaysFalse; } + @Override + public boolean isInferenceEnabled() { + return getQueryLimits().isInferenceEnabled(); + } + public SelectorImpl getSelector() { return selector; } diff --git a/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java b/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java index 7d814496c0..cffaa55159 100644 --- a/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java +++ b/oak-it-osgi/src/test/java/org/apache/jackrabbit/oak/osgi/OSGiIT.java @@ -71,6 +71,11 @@ public class OSGiIT { mavenBundle( "org.apache.felix", "org.apache.felix.configadmin", "1.9.20" ), mavenBundle( "org.apache.felix", "org.apache.felix.fileinstall", "3.2.6" ), mavenBundle( "org.ops4j.pax.logging", "pax-logging-api", "1.7.2" ), + // Jackson dependency for object serialisation. + mavenBundle().groupId("com.fasterxml.jackson.core").artifactId("jackson-core").version("2.17.2"), + mavenBundle().groupId("com.fasterxml.jackson.core").artifactId("jackson-annotations").version("2.17.2"), + mavenBundle().groupId("com.fasterxml.jackson.core").artifactId("jackson-databind").version("2.17.2"), + frameworkProperty("repository.home").value("target"), systemProperties(new SystemPropertyOption("felix.fileinstall.dir").value(getConfigDir())), jarBundles(), diff --git a/oak-query-spi/pom.xml b/oak-query-spi/pom.xml index d23ee1ed35..941d8a1906 100644 --- a/oak-query-spi/pom.xml +++ b/oak-query-spi/pom.xml @@ -82,6 +82,15 @@ <artifactId>oak-store-spi</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>${jackson.version}</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> <!-- Nullability annotations --> <dependency> diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java index 83b4ff52c8..848b445822 100644 --- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java +++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/Filter.java @@ -179,6 +179,14 @@ public interface Filter { */ boolean isAlwaysFalse(); + /** + * + * @return true if inference is enabled. + */ + default boolean isInferenceEnabled() { + return false; + } + /** * A restriction for a property. */ diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java index d06edaf520..050e1bf9da 100644 --- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java +++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryLimits.java @@ -64,4 +64,8 @@ public interface QueryLimits { return new String[] {}; } + default boolean isInferenceEnabled(){ + return false; + }; + } diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQuery.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQuery.java new file mode 100644 index 0000000000..317fb86c9d --- /dev/null +++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQuery.java @@ -0,0 +1,100 @@ +/* + * 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.spi.query.fulltext; + +import org.apache.jackrabbit.oak.json.JsonUtils; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InferenceQuery { + private static final Logger LOG = LoggerFactory.getLogger(InferenceQuery.class); + private static final String DEFAULT_INFERENCE_QUERY_CONFIG_PREFIX = "?"; + private static final String INFERENCE_QUERY_CONFIG_PREFIX_KEY = "org.apache.jackrabbit.oak.search.inference.query.prefix"; + public static final String INFERENCE_QUERY_CONFIG_PREFIX = System.getProperty( + INFERENCE_QUERY_CONFIG_PREFIX_KEY, DEFAULT_INFERENCE_QUERY_CONFIG_PREFIX); + + private final String queryInferenceConfig; + private final String queryText; + + public InferenceQuery(@NotNull String text) { + String[] components = parseText(text); + this.queryInferenceConfig = components[0]; + this.queryText = components[1]; + } + + private String[] parseText(String inputText) { + String text = inputText.trim(); + // Remove the first delimiter + if (text.startsWith(INFERENCE_QUERY_CONFIG_PREFIX) && text.charAt(INFERENCE_QUERY_CONFIG_PREFIX.length()) == '{') { + text = text.substring(INFERENCE_QUERY_CONFIG_PREFIX.length()); + + // Try to find the end of the JSON part by parsing incrementally + int possibleEndIndex = 0; + String jsonPart = null; + String queryTextPart; + int jsonEndDelimiterIndex = -1; + + while (possibleEndIndex < text.length()) { + possibleEndIndex = text.indexOf(INFERENCE_QUERY_CONFIG_PREFIX, possibleEndIndex + 1); + if (possibleEndIndex == -1) { + // If we reach here, it means we couldn't find a valid JSON part + jsonPart = ""; + LOG.warn("Query starts with inference prefix {}, but without valid json part," + + " if case this prefix is a valid fulltext query prefix, please update system property {} with different prefix value", + INFERENCE_QUERY_CONFIG_PREFIX, INFERENCE_QUERY_CONFIG_PREFIX_KEY); + break; + } + String candidateJson = text.substring(0, possibleEndIndex); + // Verify if this is valid JSON using Oak's JsopTokenizer + if (JsonUtils.isValidJson(candidateJson, false)) { + jsonPart = candidateJson; + jsonEndDelimiterIndex = possibleEndIndex; + break; + } + } + // If we found a valid JSON part, extract it + if (jsonPart == null) { + // If we reach here, it means we couldn't find a valid JSON part + jsonPart = ""; + queryTextPart = text; + LOG.warn("Query starts with InferenceQueryPrefix: {}, but without valid json part," + + " if case this prefix is a valid fulltext query prefix, please update {} with different prefix value", + INFERENCE_QUERY_CONFIG_PREFIX, INFERENCE_QUERY_CONFIG_PREFIX_KEY); + + } else { + // Extract query text part (everything after the JSON part delimiter) + queryTextPart = text.substring(jsonEndDelimiterIndex + 1).trim(); + + } + return new String[]{jsonPart, queryTextPart}; + } else { + return new String[]{"", text}; + } + } + + public String getQueryInferenceConfig() { + return queryInferenceConfig; + } + + public String getQueryText() { + return queryText; + } +} \ No newline at end of file diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfig.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfig.java new file mode 100644 index 0000000000..63ca7d6eab --- /dev/null +++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfig.java @@ -0,0 +1,54 @@ +/* + * 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.spi.query.fulltext; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class InferenceQueryConfig { + public static final String TYPE = "inferenceModelConfig"; + @Nullable + private final String inferenceModelConfig; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public InferenceQueryConfig(@NotNull String queryConfig) { + if (queryConfig.isBlank()){ + this.inferenceModelConfig = null; + return; + } else if (queryConfig.equals("{}")) { + // in this case a default inferenceModelConfig will be used. + this.inferenceModelConfig = ""; + } else { + try { + JsonNode jsonNode1 = objectMapper.readTree(queryConfig); + inferenceModelConfig = jsonNode1.get(TYPE).asText(); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error parsing inference query config: "+ queryConfig + "error message:" + e.getMessage()); + } + } + } + + public @Nullable String getInferenceModelConfig() { + return inferenceModelConfig; + } +} diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java index 324e2d104a..9e851fadb9 100644 --- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java +++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/fulltext/package-info.java @@ -18,7 +18,7 @@ /** * This package contains fulltext search condition implementations. */ -@Version("1.0.0") +@Version("1.1.0") package org.apache.jackrabbit.oak.spi.query.fulltext; import org.osgi.annotation.versioning.Version; diff --git a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java index 2cdf053fcc..200a10dc57 100644 --- a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java +++ b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/package-info.java @@ -18,7 +18,7 @@ /** * This package contains oak query index related classes. */ -@Version("3.1.0") +@Version("3.2.0") package org.apache.jackrabbit.oak.spi.query; import org.osgi.annotation.versioning.Version; diff --git a/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfigTest.java b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfigTest.java new file mode 100644 index 0000000000..4ac85ddfd1 --- /dev/null +++ b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryConfigTest.java @@ -0,0 +1,53 @@ +/* + * 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.spi.query.fulltext; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class InferenceQueryConfigTest { + + @Test + public void testEmptyConfig() { + InferenceQueryConfig config = new InferenceQueryConfig(""); + assertNull(config.getInferenceModelConfig()); + } + + @Test + public void testEmptyJsonConfig() { + InferenceQueryConfig config = new InferenceQueryConfig("{}"); + assertEquals("", config.getInferenceModelConfig()); + } + + @Test + public void testValidConfig() { + InferenceQueryConfig config = new InferenceQueryConfig("{\"inferenceModelConfig\":\"ada-test-model\"}"); + assertEquals("ada-test-model", config.getInferenceModelConfig()); + } + + @Test(expected = RuntimeException.class) + public void testInvalidJsonConfig() { + new InferenceQueryConfig("{invalid json}"); + } + + @Test(expected = RuntimeException.class) + public void testMissingTypeConfig() { + new InferenceQueryConfig("{\"someOtherField\":\"value\"}"); + } +} diff --git a/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryTest.java b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryTest.java new file mode 100644 index 0000000000..3017464b88 --- /dev/null +++ b/oak-query-spi/src/test/java/org/apache/jackrabbit/oak/spi/query/fulltext/InferenceQueryTest.java @@ -0,0 +1,92 @@ +/* + * 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.spi.query.fulltext; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class InferenceQueryTest { + + @Test + public void testBasicQuery() { + InferenceQuery query = new InferenceQuery("simple query"); + assertEquals("", query.getQueryInferenceConfig()); + assertEquals("simple query", query.getQueryText()); + } + + @Test + public void testQueryWithInferenceConfig() { + InferenceQuery query = new InferenceQuery("?{\"model\":\"gpt-4\"}?search for oak trees"); + assertEquals("{\"model\":\"gpt-4\"}", query.getQueryInferenceConfig()); + assertEquals("search for oak trees", query.getQueryText()); + } + + @Test + public void testQueryWithComplexInferenceConfig() { + InferenceQuery query = new InferenceQuery( + "?{\"model\":\"gpt-4\",\"temperature\":0.7,\"options\":{\"filter\":true}}?oak trees"); + assertEquals("{\"model\":\"gpt-4\",\"temperature\":0.7,\"options\":{\"filter\":true}}", + query.getQueryInferenceConfig()); + assertEquals("oak trees", query.getQueryText()); + } + + @Test + public void testQueryWithQuestionMarksInText() { + InferenceQuery query = new InferenceQuery("?{\"model\":\"gpt-4\"}?what are oak trees?"); + assertEquals("{\"model\":\"gpt-4\"}", query.getQueryInferenceConfig()); + assertEquals("what are oak trees?", query.getQueryText()); + } + + @Test + public void testQueryWithoutInferencePrefix() { + InferenceQuery query = new InferenceQuery("{\"model\":\"gpt-4\"}?query"); + assertEquals("", query.getQueryInferenceConfig()); + assertEquals("{\"model\":\"gpt-4\"}?query", query.getQueryText()); + } + + @Test + public void testQueryWithInvalidJson() { + InferenceQuery query = new InferenceQuery("?{invalid json}?query"); + assertEquals("", query.getQueryInferenceConfig()); + assertEquals("{invalid json}?query", query.getQueryText()); + } + + @Test + public void testQueryWithEmptyConfig() { + InferenceQuery query = new InferenceQuery("??query text"); + assertEquals("", query.getQueryInferenceConfig()); + assertEquals("??query text", query.getQueryText()); + } + + @Test + public void testQueryWithWhitespace() { + InferenceQuery query = new InferenceQuery(" ?{\"model\":\"gpt-4\"}? search query "); + assertEquals("{\"model\":\"gpt-4\"}", query.getQueryInferenceConfig()); + assertEquals("search query", query.getQueryText()); + } + + @Test + public void testEmptyQuery() { + InferenceQuery query = new InferenceQuery(""); + assertEquals("", query.getQueryInferenceConfig()); + assertEquals("", query.getQueryText()); + } + +} \ No newline at end of file diff --git a/oak-store-spi/pom.xml b/oak-store-spi/pom.xml index 5245a5b121..ba34c07de9 100644 --- a/oak-store-spi/pom.xml +++ b/oak-store-spi/pom.xml @@ -117,6 +117,11 @@ <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>${jackson.version}</version> + </dependency> <!-- Logging --> <dependency> diff --git a/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonUtils.java b/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonUtils.java new file mode 100644 index 0000000000..590403b8c2 --- /dev/null +++ b/oak-store-spi/src/main/java/org/apache/jackrabbit/oak/json/JsonUtils.java @@ -0,0 +1,281 @@ +/* + * 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.commons.json.JsopReader; +import org.apache.jackrabbit.oak.commons.json.JsopTokenizer; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonUtils { + + private static final ObjectMapper mapper = new ObjectMapper(); + + /** + * Convert a NodeState to a Map representation + * + * @param nodeState The NodeState to convert + * @param maxDepth Maximum depth to traverse + * @return Map representation of the NodeState + */ + public static Map<String, Object> convertNodeStateToMap(NodeState nodeState, int maxDepth, boolean shouldSerializeHiddenNodesOrProperties) { + return convertNodeStateToMap(nodeState, maxDepth, -1, shouldSerializeHiddenNodesOrProperties); + } + + /** + * Convert a NodeState to a Map representation + * + * @param nodeState The NodeState to convert + * @param maxDepth Maximum depth to traverse + * @param currentDepth Current traversal depth + * @return Map representation of the NodeState + */ + private static Map<String, Object> convertNodeStateToMap(NodeState nodeState, int maxDepth, int currentDepth, boolean shouldSerializeHiddenNodesOrProperties) { + if (maxDepth >= 0 && currentDepth >= maxDepth) { + return null; + } + + Map<String, Object> result = new HashMap<>(); + + // Convert properties + for (PropertyState property : nodeState.getProperties()) { + String name = property.getName(); + Type<?> type = property.getType(); + // Skip serializing hidden properties. + if (!shouldSerializeHiddenNodesOrProperties && name.startsWith(":")) { + continue; + } + if (property.isArray()) { + if (type == Type.STRINGS) { + result.put(name, property.getValue(Type.STRINGS)); + } else if (type == Type.LONGS) { + result.put(name, property.getValue(Type.LONGS)); + } else if (type == Type.DOUBLES) { + result.put(name, property.getValue(Type.DOUBLES)); + } else if (type == Type.BOOLEANS) { + result.put(name, property.getValue(Type.BOOLEANS)); + } else if (type == Type.DATES) { + result.put(name, property.getValue(Type.DATES)); + } else if (type == Type.DECIMALS) { + result.put(name, property.getValue(Type.DECIMALS)); + } else { + // For other array types, convert to strings + List<String> values = new ArrayList<>(); + for (int i = 0; i < property.count(); i++) { + values.add(property.getValue(Type.STRING, i)); + } + result.put(name, values); + } + } else { + if (type == Type.STRING) { + result.put(name, property.getValue(Type.STRING)); + } else if (type == Type.LONG) { + result.put(name, property.getValue(Type.LONG)); + } else if (type == Type.DOUBLE) { + result.put(name, property.getValue(Type.DOUBLE)); + } else if (type == Type.BOOLEAN) { + result.put(name, property.getValue(Type.BOOLEAN)); + } else if (type == Type.DATE) { + result.put(name, property.getValue(Type.DATE)); + } else if (type == Type.DECIMAL) { + result.put(name, property.getValue(Type.DECIMAL)); + } else { + // For other types, convert to string + result.put(name, property.getValue(Type.STRING)); + } + } + } + + // Convert child nodes recursively + for (String childName : nodeState.getChildNodeNames()) { + if (!shouldSerializeHiddenNodesOrProperties && childName.startsWith(":")) { + continue; + } + NodeState childNode = nodeState.getChildNode(childName); + Map<String, Object> childMap = convertNodeStateToMap(childNode, maxDepth, currentDepth + 1, shouldSerializeHiddenNodesOrProperties); + if (childMap != null) { + result.put(childName, childMap); + } + } + return result; + } + + /** + * Converts a NodeState to JSON string with specified depth + * + * @param nodeState The NodeState to convert + * @param maxDepth Maximum depth to traverse, use -1 for unlimited depth + * @return JSON string representation + * @throws JsonProcessingException if JSON processing fails + */ + public static String nodeStateToJson(NodeState nodeState, int maxDepth) throws JsonProcessingException { + JsonNode jsonNode = convertNodeStateToJson(nodeState, maxDepth, 0); + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode); + } + + private static JsonNode convertNodeStateToJson(NodeState nodeState, int maxDepth, int currentDepth) { + ObjectNode result = mapper.createObjectNode(); + + // Return if max depth reached + if (maxDepth != -1 && currentDepth > maxDepth) { + return result; + } + + // Convert properties + for (PropertyState property : nodeState.getProperties()) { + String name = property.getName(); + Type<?> type = property.getType(); + + if (property.isArray()) { + ArrayNode arrayNode = mapper.createArrayNode(); + if (type == Type.STRINGS) { + property.getValue(Type.STRINGS).forEach(arrayNode::add); + } else if (type == Type.LONGS) { + property.getValue(Type.LONGS).forEach(arrayNode::add); + } else if (type == Type.DOUBLES) { + property.getValue(Type.DOUBLES).forEach(arrayNode::add); + } else if (type == Type.BOOLEANS) { + property.getValue(Type.BOOLEANS).forEach(arrayNode::add); + } else if (type == Type.DATES) { + property.getValue(Type.DATES).forEach(arrayNode::add); + } else if (type == Type.DECIMALS) { + property.getValue(Type.DECIMALS).forEach(arrayNode::add); + } else { + // For other array types, convert to strings + for (int i = 0; i < property.count(); i++) { + arrayNode.add(property.getValue(Type.STRING, i)); + } + } + result.set(name, arrayNode); + } else { + if (type == Type.STRING) { + result.put(name, property.getValue(Type.STRING)); + } else if (type == Type.LONG) { + result.put(name, property.getValue(Type.LONG)); + } else if (type == Type.DOUBLE) { + result.put(name, property.getValue(Type.DOUBLE)); + } else if (type == Type.BOOLEAN) { + result.put(name, property.getValue(Type.BOOLEAN)); + } else if (type == Type.DATE) { + result.put(name, property.getValue(Type.DATE).toString()); + } else if (type == Type.DECIMAL) { + result.put(name, property.getValue(Type.DECIMAL).toString()); + } else { + // For other types, convert to string + result.put(name, property.getValue(Type.STRING)); + } + } + } + + // Convert child nodes recursively + for (String childName : nodeState.getChildNodeNames()) { + NodeState childNode = nodeState.getChildNode(childName); + result.set(childName, convertNodeStateToJson(childNode, maxDepth, currentDepth + 1)); + } + + return result; + } + + public static boolean isValidJson(String text, boolean isJsonArray) { + if (text == null) { + return false; + } + + JsopReader reader = new JsopTokenizer(text); + return validateJson(reader, isJsonArray); + } + + private static boolean validateJson(JsopReader reader, boolean isJsonArray) { + if (reader.matches('{')) { + return validateObject(reader) && reader.read() == JsopReader.END; + } + else if (reader.matches('[')) { + if (!isJsonArray) { + return false; + } + return validateArray(reader) && reader.read() == JsopReader.END; + } + else { + return false;// readJsonValue(reader) && reader.read() == JsopReader.END; + } + } + + private static boolean validateObject(JsopReader reader) { + boolean first = true; + while (!reader.matches('}')) { + if (!first && !reader.matches(',')) { + return false; + } + if (!reader.matches(JsopReader.STRING)) { + return false; + } + if (!reader.matches(':')) { + return false; + } + if (!readJsonValue(reader)) { + return false; + } + first = false; + } + return true; + } + + private static boolean validateArray(JsopReader reader) { + boolean first = true; + while (!reader.matches(']')) { + if (!first && !reader.matches(',')) { + return false; + } + if (!readJsonValue(reader)) { + return false; + } + first = false; + } + return true; + } + + private static boolean readJsonValue(JsopReader reader) { + int token = reader.read(); + switch (token) { + case JsopReader.STRING: + case JsopReader.NUMBER: + case JsopReader.TRUE: + case JsopReader.FALSE: + case JsopReader.NULL: + return true; + case '{': + return validateObject(reader); + case '[': + return validateArray(reader); + default: + return false; + } + } +} diff --git a/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonUtilsTest.java b/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonUtilsTest.java new file mode 100644 index 0000000000..c3848bbea0 --- /dev/null +++ b/oak-store-spi/src/test/java/org/apache/jackrabbit/oak/json/JsonUtilsTest.java @@ -0,0 +1,270 @@ +/* + * 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 org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState; +import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class JsonUtilsTest { + + @Test + public void testNullInput() { + assertFalse(JsonUtils.isValidJson(null, false)); + assertFalse(JsonUtils.isValidJson(null, true)); + } + + @Test + public void testEmptyString() { + assertFalse(JsonUtils.isValidJson("", false)); + assertFalse(JsonUtils.isValidJson("", true)); + } + + @Test + public void testValidJsonObject() { + assertTrue(JsonUtils.isValidJson("{}", false)); + assertTrue(JsonUtils.isValidJson("{\"key\":\"value\"}", false)); + assertTrue(JsonUtils.isValidJson("{\"key\":123}", false)); + assertTrue(JsonUtils.isValidJson("{\"key\":true}", false)); + assertTrue(JsonUtils.isValidJson("{\"key\":null}", false)); + assertTrue(JsonUtils.isValidJson("{\"key\":{\"nested\":\"value\"}}", false)); + } + + @Test + public void testInvalidJsonObject() { + assertFalse(JsonUtils.isValidJson("{", false)); + assertFalse(JsonUtils.isValidJson("}", false)); + assertFalse(JsonUtils.isValidJson("{key:value}", false)); + assertFalse(JsonUtils.isValidJson("{\"key\":value}", false)); + assertFalse(JsonUtils.isValidJson("{\"key\":\"value\"", false)); + assertFalse(JsonUtils.isValidJson("{\"key\":\"value\",}", false)); + } + + @Test + public void testValidJsonArray() { + assertTrue(JsonUtils.isValidJson("[]", true)); + assertTrue(JsonUtils.isValidJson("[1,2,3]", true)); + assertTrue(JsonUtils.isValidJson("[\"a\",\"b\",\"c\"]", true)); + assertTrue(JsonUtils.isValidJson("[true,false,null]", true)); + assertTrue(JsonUtils.isValidJson("[{\"key\":\"value\"},{\"key2\":123}]", true)); + } + + @Test + public void testInvalidJsonArray() { + assertFalse(JsonUtils.isValidJson("[", true)); + assertFalse(JsonUtils.isValidJson("]", true)); + assertFalse(JsonUtils.isValidJson("[1,2,]", true)); + assertFalse(JsonUtils.isValidJson("[1 2 3]", true)); + assertFalse(JsonUtils.isValidJson("[\"unclosed]", true)); + } + + @Test + public void testArrayNotAllowedWhenFlagIsFalse() { + assertFalse(JsonUtils.isValidJson("[]", false)); + assertFalse(JsonUtils.isValidJson("[1,2,3]", false)); + } + + @Test + public void testValidPrimitiveValues() { + assertFalse(JsonUtils.isValidJson("123", false)); + assertFalse(JsonUtils.isValidJson("\"string\"", false)); + assertFalse(JsonUtils.isValidJson("true", false)); + assertFalse(JsonUtils.isValidJson("false", false)); + assertFalse(JsonUtils.isValidJson("null", false)); + } + + @Test + public void testInvalidPrimitiveValues() { + assertFalse(JsonUtils.isValidJson("undefined", false)); + assertFalse(JsonUtils.isValidJson("'string'", false)); + assertFalse(JsonUtils.isValidJson("TRUE", false)); + assertFalse(JsonUtils.isValidJson("False", false)); + assertFalse(JsonUtils.isValidJson("NULL", false)); + } + + @Test + public void testComplexNestedStructures() { + assertTrue(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":true}]}", false)); + assertTrue(JsonUtils.isValidJson("[{\"obj\":{}},[[],{}]]", true)); + assertTrue(JsonUtils.isValidJson("{\"a\":{\"b\":{\"c\":{\"d\":null}}}}", false)); + assertFalse(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":Undefined}]}", false)); + assertFalse(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":NaN}]}", false)); + assertFalse(JsonUtils.isValidJson("{\"array\":[1,2,{\"nested\":Infinity}]}", false)); + assertFalse(JsonUtils.isValidJson("{\"a\":{\"b\":{\"c\":{\"d\":void}}}}", false)); + } + + @Test + public void testConvertNodeStateToMap() { + MemoryNodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE); + builder.setProperty("property1", "value1"); + builder.setProperty("property2", 123); + NodeState nodeState = builder.getNodeState(); + + Map<String, Object> result = JsonUtils.convertNodeStateToMap(nodeState, 2, true); + + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.containsKey("property1")); + assertTrue(result.containsKey("property2")); + assertEquals("value1", result.get("property1")); + assertEquals(123L, result.get("property2")); + } + + @Test + public void testConvertNodeStateToMapWithMaxDepth() { + MemoryNodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE); + builder.setProperty("property1", "value1"); + NodeState nodeState = builder.getNodeState(); + + Map<String, Object> result = JsonUtils.convertNodeStateToMap(nodeState, 0, true); + assertNotNull(result); + } + + @Test + public void testConvertNodeStateToMapWithNestedNodes() { + NodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE); + builder.setProperty("prop1", "val1"); + + NodeBuilder child = builder.child("child1"); + child.setProperty("childProp1", "childVal1"); + + NodeBuilder grandChild = child.child("grandChild1"); + grandChild.setProperty("grandChildProp1", "grandChildVal1"); + + NodeBuilder greatGrandChild = grandChild.child("greatGrandChild1"); + greatGrandChild.setProperty("greatGrandChildProp1", "greatGrandChildVal1"); + + NodeState nodeState = builder.getNodeState(); + + // Test with maxDepth = 0 (should return only properties on current node) + Map<String, Object> result0 = JsonUtils.convertNodeStateToMap(nodeState, 0, true); + assertNotNull(result0); + assertEquals(1, result0.size()); + assertTrue(result0.containsKey("prop1")); + assertEquals("val1", result0.get("prop1")); + assertNotNull(result0); + assertTrue(result0.containsKey("prop1")); + assertFalse(result0.containsKey("child1")); + + + // Test with maxDepth = 1 (should include child1) + Map<String, Object> result1 = JsonUtils.convertNodeStateToMap(nodeState, 1, true); + assertNotNull(result1); + assertTrue(result1.containsKey("child1")); + Map<String, Object> childMap1 = (Map<String, Object>) result1.get("child1"); + assertEquals("childVal1", childMap1.get("childProp1")); + assertNotNull(result1); + assertTrue(result1.containsKey("prop1")); + assertTrue(result1.containsKey("child1")); + assertFalse(childMap1.containsKey("grandChild1")); + + + // Test with maxDepth = 2 (should include grandChild1) + Map<String, Object> result2 = JsonUtils.convertNodeStateToMap(nodeState, 2, true); + Map<String, Object> childMap2 = (Map<String, Object>) result2.get("child1"); + assertTrue(childMap2.containsKey("grandChild1")); + Map<String, Object> grandChildMap2 = (Map<String, Object>) childMap2.get("grandChild1"); + assertEquals("grandChildVal1", grandChildMap2.get("grandChildProp1")); + assertNotNull(result2); + assertTrue(result2.containsKey("prop1")); + assertTrue(result2.containsKey("child1")); + assertFalse(grandChildMap2.containsKey("greatGrandChild1")); + + // Test with maxDepth = 3 (should include greatGrandChild1) + Map<String, Object> result3 = JsonUtils.convertNodeStateToMap(nodeState, 3, true); + Map<String, Object> childMap3 = (Map<String, Object>) result3.get("child1"); + Map<String, Object> grandChildMap3 = (Map<String, Object>) childMap3.get("grandChild1"); + Map<String, Object> greatGrandChildMap3 = (Map<String, Object>) grandChildMap3.get("greatGrandChild1"); + assertEquals("greatGrandChildVal1", greatGrandChildMap3.get("greatGrandChildProp1")); + assertNotNull(result3); + assertTrue(result3.containsKey("prop1")); + assertTrue(result3.containsKey("child1")); + + // Test with maxDepth = -1 (should return all nodes and properties) + Map<String, Object> resultNeg1 = JsonUtils.convertNodeStateToMap(nodeState, -1, true); + Map<String, Object> childMapNeg1 = (Map<String, Object>) resultNeg1.get("child1"); + Map<String, Object> grandChildMapNeg1 = (Map<String, Object>) childMapNeg1.get("grandChild1"); + assertTrue(grandChildMapNeg1.containsKey("greatGrandChild1")); + Map<String, Object> greatGrandChildMapNeg1 = (Map<String, Object>) grandChildMapNeg1.get("greatGrandChild1"); + assertEquals("greatGrandChildVal1", greatGrandChildMapNeg1.get("greatGrandChildProp1")); + assertNotNull(resultNeg1); + assertTrue(resultNeg1.containsKey("prop1")); + assertTrue(resultNeg1.containsKey("child1")); + + + } + + @Test + public void testConvertNodeStateToMapWithHiddenNodesAndProperties() { + NodeBuilder builder = new MemoryNodeBuilder(EmptyNodeState.EMPTY_NODE); + + // Regular property + builder.setProperty("regularProp", "regularValue"); + + // Hidden property (starts with ":") + builder.setProperty(":hiddenProp", "hiddenValue"); + + // Regular child + NodeBuilder regularChild = builder.child("regularChild"); + regularChild.setProperty("childProp", "childValue"); + + // Hidden child (starts with ":") + NodeBuilder hiddenChild = builder.child(":hiddenChild"); + hiddenChild.setProperty("hiddenChildProp", "hiddenChildValue"); + + NodeState nodeState = builder.getNodeState(); + + // Test with shouldSerializeHiddenNodesOrProperties = true + // Hidden nodes and properties should be excluded + Map<String, Object> resultWithHiddenExcluded = JsonUtils.convertNodeStateToMap(nodeState, -1, false); + assertNotNull(resultWithHiddenExcluded); + assertEquals(2, resultWithHiddenExcluded.size()); + assertTrue(resultWithHiddenExcluded.containsKey("regularProp")); + assertTrue(resultWithHiddenExcluded.containsKey("regularChild")); + assertFalse(resultWithHiddenExcluded.containsKey(":hiddenProp")); + assertFalse(resultWithHiddenExcluded.containsKey(":hiddenChild")); + + // Test with shouldSerializeHiddenNodesOrProperties = false + // Hidden nodes and properties should be included + Map<String, Object> resultWithHiddenIncluded = JsonUtils.convertNodeStateToMap(nodeState, -1, true); + assertNotNull(resultWithHiddenIncluded); + assertEquals(4, resultWithHiddenIncluded.size()); + assertTrue(resultWithHiddenIncluded.containsKey("regularProp")); + assertTrue(resultWithHiddenIncluded.containsKey("regularChild")); + assertTrue(resultWithHiddenIncluded.containsKey(":hiddenProp")); + assertTrue(resultWithHiddenIncluded.containsKey(":hiddenChild")); + assertEquals("hiddenValue", resultWithHiddenIncluded.get(":hiddenProp")); + + // Verify nested content + Map<String, Object> regularChildMap = (Map<String, Object>) resultWithHiddenIncluded.get("regularChild"); + assertEquals("childValue", regularChildMap.get("childProp")); + + Map<String, Object> hiddenChildMap = (Map<String, Object>) resultWithHiddenIncluded.get(":hiddenChild"); + assertEquals("hiddenChildValue", hiddenChildMap.get("hiddenChildProp")); + } +}
