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

Reply via email to