Repository: networknt/json-schema-validator
Pull request #1059 (2024-06-12 00:49:05)

Deprecate config for unevaluatedItems and unevaluatedProperties
Deprecate config for `unevaluatedItems` and `unevaluatedProperties` as this is no longer needed and doesn't do anything since the validators were rewritten to use annotations as it doesn't cost additional performance by collecting annotations if not needed and will always collect annotations for the relevant validators if needed.

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1059?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) Report
Attention: Patch coverage is `0%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 78.76%. Comparing base [(`48ca3c2`)](https://app.codecov.io/gh/networknt/json-schema-validator/commit/48ca3c22995e32797e9defdb8b77869f0d15caf3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) to head [(`8d65876`)](https://app.codecov.io/gh/networknt/json-schema-validator/commit/8d65876475e50793754cd98c5b39a6b85a74ef9e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).
> Report is 25 commits behind head on master.

| [Files](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1059?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) | Patch % | Lines |
|---|---|---|
| [...a/com/networknt/schema/SchemaValidatorsConfig.java](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1059?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Fcom%2Fnetworknt%2Fschema%2FSchemaValidatorsConfig.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9TY2hlbWFWYWxpZGF0b3JzQ29uZmlnLmphdmE=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1059?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##             master    #1059      +/-   ##
============================================
- Coverage     78.90%   78.76%   -0.15%     
- Complexity     1965     2018      +53     
============================================
  Files           172      190      +18     
  Lines          6352     6521     +169     
  Branches       1255     1245      -10     
============================================
+ Hits           5012     5136     +124     
- Misses          867      913      +46     
+ Partials        473      472       -1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/networknt/json-schema-validator/pull/1059?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt).


Patch:
diff --git a/README.md b/README.md
index 8c6c090..cfb0f92 100644
--- a/README.md
+++ b/README.md
@@ -157,14 +157,26 @@ The following are the optional dependencies that may be required for certain opt
 These are not automatically included and setting the relevant option without adding the library will result in a `ClassNotFoundException`.
 
 ```xml
-<!-- This is required when setting setEcma262Validator(true)  -->
+<!-- Either library is required when setting setEcma262Validator(true) or explicitly via setRegularExpressionFactory() -->
 <dependency>
     <!-- Used to validate ECMA 262 regular expressions -->
+    <!-- Approximately 2 MB in dependencies -->
+    <!-- JoniRegularExpressionFactory -->
     <groupId>org.jruby.joni</groupId>
     <artifactId>joni</artifactId>
     <version>${version.joni}</version>
     <optional>true</optional>
 </dependency>
+
+<dependency>
+    <!-- Used to validate ECMA 262 regular expressions -->
+    <!-- Approximately 50 MB in dependencies -->
+    <!-- GraalJSRegularExpressionFactory -->
+    <groupId>org.graalvm.js</groupId>
+    <artifactId>js</artifactId>
+    <version>${version.graaljs}</version>
+    <optional>true</optional>
+</dependency>
 ```
 
 ##### Excludable Dependencies
@@ -258,7 +270,7 @@ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
 // By default JSON Path is used for reporting the instance location and evaluation path
 config.setPathType(PathType.JSON_POINTER);
 // By default the JDK regular expression implementation which is not ECMA 262 compliant is used
-// Note that setting this to true requires including the optional joni dependency
+// Note that setting this to true requires including the optional joni or graaljs dependency
 // config.setEcma262Validator(true);
 
 // Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json.
@@ -293,7 +305,7 @@ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
 // By default JSON Path is used for reporting the instance location and evaluation path
 config.setPathType(PathType.JSON_POINTER);
 // By default the JDK regular expression implementation which is not ECMA 262 compliant is used
-// Note that setting this to true requires including the optional joni dependency
+// Note that setting this to true requires including the optional joni or graaljs dependency
 // config.setEcma262Validator(true);
 
 // Due to the mapping the meta-schema will be retrieved from the classpath at classpath:draft/2020-12/schema.
@@ -517,7 +529,7 @@ The following is sample output from the Hierarchical format.
 | Name                                  | Description                                                                                                                                                                                                                       | Default Value
 |---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------
 | `pathType`                            | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_POINTER` to use JSON Pointer.                                                                                                 | `PathType.DEFAULT`
-| `ecma262Validator`                    | Whether to use the ECMA 262 `joni` library to validate the `pattern` keyword. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown.                                      | `false`
+| `ecma262Validator`                    | Whether to use the ECMA 262 `joni` or `graaljs` library to validate the `pattern` keyword. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown.                         | `false`
 | `executionContextCustomizer`          | This can be used to customize the `ExecutionContext` generated by the `JsonSchema` for each validation run.                                                                                                                       | `null`
 | `schemaIdValidator`                   | This is used to customize how the `$id` values are validated. Note that the default implementation allows non-empty fragments where no base IRI is specified and also allows non-absolute IRI `$id` values in the root schema.    | `JsonSchemaIdValidator.DEFAULT`
 | `messageSource`                       | This is used to retrieve the locale specific messages.                                                                                                                                                                            | `DefaultMessageSource.getInstance()`
@@ -527,6 +539,7 @@ The following is sample output from the Hierarchical format.
 | `locale`                              | The locale to use for generating messages in the `ValidationMessage`.                                                                                                                                                             | `Locale.getDefault()`
 | `failFast`                            | Whether to return failure immediately when an assertion is generated.                                                                                                                                                             | `false`
 | `formatAssertionsEnabled`             | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior.                                        | `null`
+| `regularExpressionFactory`            | The factory to use to create regular expressions for instance `JoniRegularExpressionFactory` or `GraalJSRegularExpressionFactory`.                                                                                                | `JDKRegularExpressionFactory.getInstance()`
 
 ## Performance Considerations
 
diff --git a/pom.xml b/pom.xml
index 1959b65..1cc1393 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,8 +75,9 @@
     <version.itu>1.10.2</version.itu>
     <version.jackson>2.17.0</version.jackson>
     <version.joni>2.2.1</version.joni>
-    <version.logback>1.3.14</version.logback>
+    <version.logback>1.3.14</version.logback> <!-- 1.4.x and above is not Java 8 compatible -->
     <version.slf4j>2.0.13</version.slf4j>
+    <version.graaljs>21.3.10</version.graaljs> <!-- 22.x and above is not Java 8 compatible -->
 
     <version.hamcrest>2.2</version.hamcrest>
     <version.junit>5.10.2</version.junit>
@@ -138,12 +139,31 @@
 
     <dependency>
       <!-- Used to validate ECMA 262 regular expressions -->
+      <!-- Approximately 2 MB in dependencies -->
       <groupId>org.jruby.joni</groupId>
       <artifactId>joni</artifactId>
       <version>${version.joni}</version>
       <optional>true</optional>
     </dependency>
 
+    <dependency>
+      <!-- Used to validate ECMA 262 regular expressions -->
+      <!-- Approximately 50 MB in dependencies -->
+      <groupId>org.graalvm.js</groupId>
+      <artifactId>js</artifactId>
+      <version>${version.graaljs}</version>
+      <optional>true</optional>
+    </dependency>
+
+    <dependency>
+      <!-- Used to validate ECMA 262 regular expressions -->
+      <!-- Approximately 50 MB in dependencies -->
+      <groupId>org.graalvm.sdk</groupId>
+      <artifactId>graal-sdk</artifactId>
+      <version>${version.graaljs}</version>
+      <optional>true</optional>
+    </dependency>
+
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index 1240da3..7284e54 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -19,6 +19,9 @@ package com.networknt.schema;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.i18n.DefaultMessageSource;
 import com.networknt.schema.i18n.MessageSource;
+import com.networknt.schema.regex.ECMAScriptRegularExpressionFactory;
+import com.networknt.schema.regex.JDKRegularExpressionFactory;
+import com.networknt.schema.regex.RegularExpressionFactory;
 import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
 import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
 import com.networknt.schema.walk.DefaultPropertyWalkListenerRunner;
@@ -59,9 +62,9 @@ public class SchemaValidatorsConfig {
     private ApplyDefaultsStrategy applyDefaultsStrategy = ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY;
 
     /**
-     * When set to true, use ECMA-262 compatible validator
+     * Used to create {@link com.networknt.schema.regex.RegularExpression}.
      */
-    private boolean ecma262Validator;
+    private RegularExpressionFactory regularExpressionFactory = JDKRegularExpressionFactory.getInstance();
 
     /**
      * When set to true, use Java-specific semantics rather than native JavaScript
@@ -168,54 +171,52 @@ public class SchemaValidatorsConfig {
 
     /************************ START OF UNEVALUATED CHECKS **********************************/
 
-    // These are costly in terms of performance so we provide a way to disable them.
-    private boolean disableUnevaluatedItems = false;
-    private boolean disableUnevaluatedProperties = false;
-    
+    @Deprecated
     public SchemaValidatorsConfig disableUnevaluatedAnalysis() {
-        disableUnevaluatedItems();
-        disableUnevaluatedProperties();
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig disableUnevaluatedItems() {
-        this.disableUnevaluatedItems = true;
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig disableUnevaluatedProperties() {
-        this.disableUnevaluatedProperties = true;
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig enableUnevaluatedAnalysis() {
-        enableUnevaluatedItems();
-        enableUnevaluatedProperties();
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig enableUnevaluatedItems() {
-        this.disableUnevaluatedItems = false;
         return this;
     }
 
+    @Deprecated
     public SchemaValidatorsConfig enableUnevaluatedProperties() {
-        this.disableUnevaluatedProperties = false;
         return this;
     }
 
+    @Deprecated
     public boolean isUnevaluatedItemsAnalysisDisabled() {
-        return this.disableUnevaluatedItems;
+        return false;
     }
 
+    @Deprecated
     public boolean isUnevaluatedItemsAnalysisEnabled() {
         return !isUnevaluatedItemsAnalysisDisabled();
     }
 
+    @Deprecated
     public boolean isUnevaluatedPropertiesAnalysisDisabled() {
-        return this.disableUnevaluatedProperties;
+        return false;
     }
 
+    @Deprecated
     public boolean isUnevaluatedPropertiesAnalysisEnabled() {
         return !isUnevaluatedPropertiesAnalysisDisabled();
     }
@@ -268,12 +269,55 @@ public class SchemaValidatorsConfig {
         this.handleNullableField = handleNullableField;
     }
 
+    /**
+     * Gets whether to use a ECMA-262 compliant regular expression validator.
+     * <p>
+     * This defaults to the false and setting true require inclusion of optional
+     * org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @return true if ECMA-262 compliant
+     */
     public boolean isEcma262Validator() {
-        return this.ecma262Validator;
+        return !(this.regularExpressionFactory instanceof JDKRegularExpressionFactory);
     }
 
+    /**
+     * Sets whether to use a ECMA-262 compliant regular expression validator.
+     * <p>
+     * This defaults to the false and setting true require inclusion of optional
+     * org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @param ecma262Validator true if ECMA-262 compliant
+     */
     public void setEcma262Validator(boolean ecma262Validator) {
-        this.ecma262Validator = ecma262Validator;
+        this.regularExpressionFactory = ecma262Validator ? ECMAScriptRegularExpressionFactory.getInstance()
+                : JDKRegularExpressionFactory.getInstance();
+    }
+
+    /**
+     * Gets the regular expression factory.
+     * <p>
+     * This defaults to the JDKRegularExpressionFactory and the implementations
+     * require inclusion of optional org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @return the factory
+     */
+    public RegularExpressionFactory getRegularExpressionFactory() {
+        return regularExpressionFactory;
+    }
+
+    /**
+     * Sets the regular expression factory.
+     * <p>
+     * This defaults to the JDKRegularExpressionFactory and the implementations
+     * require inclusion of optional org.jruby.joni:joni or org.graalvm.js:js dependencies.
+     *
+     * @see JDKRegularExpressionFactory
+     * @see ECMAScriptRegularExpressionFactory
+     * @param regularExpressionFactory the factory
+     */
+    public void setRegularExpressionFactory(RegularExpressionFactory regularExpressionFactory) {
+        this.regularExpressionFactory = regularExpressionFactory;
     }
 
     public boolean isJavaSemantics() {
diff --git a/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java
new file mode 100644
index 0000000..72c5fec
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import com.networknt.schema.utils.Classes;
+
+/**
+ * ECMAScript regular expression factory that chooses between GraalJS or Joni
+ * implementations depending on which is on the classpath.
+ */
+public class ECMAScriptRegularExpressionFactory implements RegularExpressionFactory {
+    private static final boolean JONI_PRESENT = Classes.isPresent("org.joni.Regex",
+            ECMAScriptRegularExpressionFactory.class.getClassLoader());
+    private static final boolean GRAALJS_PRESENT = Classes.isPresent("com.oracle.truffle.js.parser.GraalJSEvaluator",
+            ECMAScriptRegularExpressionFactory.class.getClassLoader());
+
+    private static final RegularExpressionFactory DELEGATE = GRAALJS_PRESENT
+            ? GraalJSRegularExpressionFactory.getInstance()
+            : JoniRegularExpressionFactory.getInstance();
+    
+    public static final ECMAScriptRegularExpressionFactory INSTANCE = new ECMAScriptRegularExpressionFactory();
+
+    public static ECMAScriptRegularExpressionFactory getInstance() {
+        if (!JONI_PRESENT && !GRAALJS_PRESENT) {
+            throw new IllegalArgumentException(
+                    "Either org.jruby.joni:joni or org.graalvm.js:js needs to be present in the classpath");
+        }
+        return INSTANCE;
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return DELEGATE.getRegularExpression(regex);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSContextFactory.java b/src/main/java/com/networknt/schema/regex/GraalJSContextFactory.java
new file mode 100644
index 0000000..7e41669
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSContextFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Context;
+
+/**
+ * Factory for the js {@link Context}.
+ */
+public class GraalJSContextFactory {
+    /**
+     * The holder defers the classloading until it is used.
+     */
+    private static class Holder {
+        private static final Context INSTANCE = Context.newBuilder("js").option("engine.WarnInterpreterOnly", "false")
+                .build();
+    }
+
+    /**
+     * Gets the singleton instance of the Context.
+     * <p>
+     * This may need to be closed to release resources if no longer needed.
+     *
+     * @return the Context
+     */
+    public static Context getInstance() {
+        return Holder.INSTANCE;
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java
new file mode 100644
index 0000000..0ab33cd
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpression.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Value;
+
+/**
+ * GraalJS {@link RegularExpression}.
+ * <p>
+ * This requires a dependency on org.graalvm.js:js which along with its
+ * dependency libraries are 50 mb.
+ */
+class GraalJSRegularExpression implements RegularExpression {
+    private final GraalJSRegularExpressionContext context;
+    private final Value function;
+    
+    GraalJSRegularExpression(String regex, GraalJSRegularExpressionContext context) {
+        this.context = context;
+        synchronized(context.getContext()) {
+            this.function = context.getRegExpBuilder().execute(regex);
+        }
+    }
+
+    @Override
+    public boolean matches(String value) {
+        synchronized(context.getContext()) {
+            return !function.execute(value).isNull();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java
new file mode 100644
index 0000000..14c63a8
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionContext.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Context;
+import org.graalvm.polyglot.Value;
+
+/**
+ * GraalJSRegularExpressionContext.
+ */
+public class GraalJSRegularExpressionContext {
+    private static final String SOURCE = "pattern => {\n"
+            + "    const regex = new RegExp(pattern, 'u');\n"
+            + "    return text => text.match(regex)\n"
+            + "};";
+
+    private final Context context;
+    private final Value regExpBuilder;
+
+    /**
+     * Constructor.
+     * <p>
+     * It is the caller's responsibility to release the context when it is no longer
+     * required.
+     * 
+     * @param context the context
+     */
+    public GraalJSRegularExpressionContext(Context context) {
+        this.context = context;
+        synchronized(this.context) {
+            this.regExpBuilder = this.context.eval("js", SOURCE);
+        }
+    }
+
+    /**
+     * Operations must synchronize on the {@link Context} as only a single thread
+     * can access the {@link Context} and {@link #getRegExpBuilder()} at one time.
+     * 
+     * @return the context
+     */
+    public Context getContext() {
+        return context;
+    }
+
+    /**
+     * Gets the RegExp builder.
+     * 
+     * @return the regexp builder
+     */
+    public Value getRegExpBuilder() {
+        return regExpBuilder;
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java
new file mode 100644
index 0000000..113da02
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/GraalJSRegularExpressionFactory.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import org.graalvm.polyglot.Context;
+
+/**
+ * GraalJS {@link RegularExpressionFactory}.
+ * <p>
+ * This requires a dependency on org.graalvm.js:js which along with its
+ * dependency libraries are 50 MB.
+ */
+public class GraalJSRegularExpressionFactory implements RegularExpressionFactory {
+    private static class Holder {
+        private static final GraalJSRegularExpressionFactory INSTANCE = new GraalJSRegularExpressionFactory();
+    }
+
+    private final GraalJSRegularExpressionContext context;
+
+    public static GraalJSRegularExpressionFactory getInstance() {
+        return Holder.INSTANCE;
+    }
+
+    /**
+     * Constructor.
+     * <p>
+     * This uses the context from {@link GraalJSContextFactory#getInstance()}.
+     * <p>
+     * It is the caller's responsibility to release the context when it is no longer
+     * required by using GraalJSContextFactory.getInstance().close().
+     */
+    public GraalJSRegularExpressionFactory() {
+        this(GraalJSContextFactory.getInstance());
+    }
+
+    /**
+     * Constructor.
+     * <p>
+     * It is the caller's responsibility to release the context when it is no longer
+     * required.
+     * 
+     * @param context the context
+     */
+    public GraalJSRegularExpressionFactory(Context context) {
+        this.context = new GraalJSRegularExpressionContext(context);
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return new GraalJSRegularExpression(regex, this.context);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/JDKRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/JDKRegularExpressionFactory.java
new file mode 100644
index 0000000..0e773a3
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/JDKRegularExpressionFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+/**
+ * JDK {@link RegularExpressionFactory}.
+ */
+public class JDKRegularExpressionFactory implements RegularExpressionFactory {
+    private static final JDKRegularExpressionFactory INSTANCE = new JDKRegularExpressionFactory();
+    
+    public static JDKRegularExpressionFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return new JDKRegularExpression(regex);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java b/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java
index 5e205e4..21cbdd1 100644
--- a/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java
+++ b/src/main/java/com/networknt/schema/regex/JoniRegularExpression.java
@@ -7,18 +7,37 @@ import org.jcodings.specific.UTF8Encoding;
 import org.joni.Option;
 import org.joni.Regex;
 import org.joni.Syntax;
+import org.joni.constants.SyntaxProperties;
 import org.joni.exception.SyntaxException;
 
 /**
- * ECMAScript {@link RegularExpression}.
+ * Joni {@link RegularExpression}.
+ * <p>
+ * This requires a dependency on org.jruby.joni:joni which along with its
+ * dependency libraries are 2 MB.
  */
 class JoniRegularExpression implements RegularExpression {
     private final Regex pattern;
     private final Pattern INVALID_ESCAPE_PATTERN = Pattern.compile(
-            ".*\\\\([aeg-moqyzACE-OQ-RT-VX-Z1-9]|c$|[pP]([^{]|$)|u([^{0-9]|$)|x([0-9a-fA-F][^0-9a-fA-F]|[^0-9a-fA-F][0-9a-fA-F]|[^0-9a-fA-F][^0-9a-fA-F]|.?$)).*");
+            ".*\\\\([aeg-jl-moqyzACE-OQ-RT-VX-Z1-9]|k([^<]|$)|c$|[pP]([^{]|$)|u([^{0-9]|$)|x([0-9a-fA-F][^0-9a-fA-F]|[^0-9a-fA-F][0-9a-fA-F]|[^0-9a-fA-F][^0-9a-fA-F]|.?$)).*");
+    
+    /**
+     * This is a custom syntax as Syntax.ECMAScript doesn't seem to be correct.
+     * <p>
+     * 
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group">OP2_QMARK_LT_NAMED_GROUP</a>
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_backreference">OP2_ESC_K_NAMED_BACKREF</a>
+     */
+    private static final Syntax SYNTAX = new Syntax(Syntax.ECMAScript.name, Syntax.ECMAScript.op,
+            Syntax.ECMAScript.op2 | SyntaxProperties.OP2_QMARK_LT_NAMED_GROUP
+                    | SyntaxProperties.OP2_ESC_K_NAMED_BACKREF,
+            Syntax.ECMAScript.op3, Syntax.ECMAScript.behavior, Syntax.ECMAScript.options,
+            Syntax.ECMAScript.metaCharTable);;
 
     JoniRegularExpression(String regex) {
-        this(regex, Syntax.ECMAScript);
+        this(regex, SYNTAX);
     }
 
     JoniRegularExpression(String regex, Syntax syntax) {
diff --git a/src/main/java/com/networknt/schema/regex/JoniRegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/JoniRegularExpressionFactory.java
new file mode 100644
index 0000000..2d451ae
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/JoniRegularExpressionFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+/**
+ * Joni {@link RegularExpressionFactory}.
+ * <p>
+ * This requires a dependency on org.jruby.joni:joni which along with its
+ * dependency libraries are 2 MB.
+ */
+public class JoniRegularExpressionFactory implements RegularExpressionFactory {
+    private static final JoniRegularExpressionFactory INSTANCE = new JoniRegularExpressionFactory();
+
+    public static JoniRegularExpressionFactory getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public RegularExpression getRegularExpression(String regex) {
+        return new JoniRegularExpression(regex);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/regex/RegularExpression.java b/src/main/java/com/networknt/schema/regex/RegularExpression.java
index 83f4d22..8a3140b 100644
--- a/src/main/java/com/networknt/schema/regex/RegularExpression.java
+++ b/src/main/java/com/networknt/schema/regex/RegularExpression.java
@@ -11,9 +11,7 @@ public interface RegularExpression {
 
     static RegularExpression compile(String regex, ValidationContext validationContext) {
         if (null == regex) return s -> true;
-        return validationContext.getConfig().isEcma262Validator()
-            ? new JoniRegularExpression(regex)
-            : new JDKRegularExpression(regex);
+        return validationContext.getConfig().getRegularExpressionFactory().getRegularExpression(regex);
     }
 
 }
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java b/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java
new file mode 100644
index 0000000..5aca068
--- /dev/null
+++ b/src/main/java/com/networknt/schema/regex/RegularExpressionFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+/**
+ * Factory for {@link RegularExpression}.
+ */
+public interface RegularExpressionFactory {
+    /**
+     * Gets a {@link RegularExpression}.
+     * 
+     * @param regex the regular expression text value
+     * @return the regular expression
+     */
+    RegularExpression getRegularExpression(String regex);
+}


Test patch:
diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java
index 3852f63..4e9e8f1 100644
--- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java
+++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTestSuite.java
@@ -18,6 +18,8 @@ package com.networknt.schema;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.networknt.schema.SpecVersion.VersionFlag;
+import com.networknt.schema.regex.JDKRegularExpressionFactory;
+import com.networknt.schema.regex.JoniRegularExpressionFactory;
 import com.networknt.schema.serialization.JsonMapperFactory;
 import com.networknt.schema.suite.TestCase;
 import com.networknt.schema.suite.TestSource;
@@ -206,7 +208,9 @@ public abstract class AbstractJsonSchemaTestSuite extends HTTPServiceSupport {
 
         SchemaValidatorsConfig config = new SchemaValidatorsConfig();
         config.setTypeLoose(typeLoose);
-        config.setEcma262Validator(TestSpec.RegexKind.JDK != testSpec.getRegex());
+        config.setRegularExpressionFactory(
+                TestSpec.RegexKind.JDK == testSpec.getRegex() ? JDKRegularExpressionFactory.getInstance()
+                        : JoniRegularExpressionFactory.getInstance());
         testSpec.getStrictness().forEach(config::setStrict);
 
         if (testSpec.getConfig() != null) {
diff --git a/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java b/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java
index 22059d4..c21d7ef 100644
--- a/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java
+++ b/src/test/java/com/networknt/schema/PatternPropertiesValidatorTest.java
@@ -19,6 +19,7 @@ package com.networknt.schema;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.SpecVersion.VersionFlag;
 import com.networknt.schema.output.OutputUnit;
+import com.networknt.schema.regex.JoniRegularExpressionFactory;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -52,7 +53,7 @@ public class PatternPropertiesValidatorTest extends BaseJsonSchemaValidatorTest
     public void testInvalidPatternPropertiesValidatorECMA262() throws Exception {
         Assertions.assertThrows(JsonSchemaException.class, () -> {
             SchemaValidatorsConfig config = new SchemaValidatorsConfig();
-            config.setEcma262Validator(true);
+            config.setRegularExpressionFactory(JoniRegularExpressionFactory.getInstance());
             JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
             JsonSchema schema = factory.getSchema("{\"patternProperties\":6}", config);
 
diff --git a/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java b/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java
new file mode 100644
index 0000000..511eac9
--- /dev/null
+++ b/src/test/java/com/networknt/schema/SchemaValidatorsConfigTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import com.networknt.schema.regex.ECMAScriptRegularExpressionFactory;
+import com.networknt.schema.regex.JDKRegularExpressionFactory;
+
+/**
+ * Test for SchemaValidatorsConfig.
+ */
+class SchemaValidatorsConfigTest {
+    @Test
+    void defaultEcma262Validator() {
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+        assertSame(JDKRegularExpressionFactory.getInstance(), config.getRegularExpressionFactory());
+        assertFalse(config.isEcma262Validator());
+    }
+
+    @Test
+    void setEcma262Validator() {
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+
+        config.setEcma262Validator(true);
+        assertSame(ECMAScriptRegularExpressionFactory.getInstance(), config.getRegularExpressionFactory());
+        assertTrue(config.isEcma262Validator());
+
+        config.setEcma262Validator(false);
+        assertSame(JDKRegularExpressionFactory.getInstance(), config.getRegularExpressionFactory());
+        assertFalse(config.isEcma262Validator());
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..15eaad3
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/ECMAScriptRegularExpressionFactoryTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test for ECMAScriptRegularExpressionFactory. 
+ */
+class ECMAScriptRegularExpressionFactoryTest {
+
+    @Test
+    void shouldPreferGraalJS() {
+        RegularExpression regularExpression = ECMAScriptRegularExpressionFactory.getInstance()
+                .getRegularExpression("[a-z]");
+        assertInstanceOf(GraalJSRegularExpression.class, regularExpression);
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..a1c4de0
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionFactoryTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.graalvm.polyglot.Context;
+import org.junit.jupiter.api.Test;
+
+/**
+ * GraalJSRegularExpressionFactoryTest.
+ */
+class GraalJSRegularExpressionFactoryTest {
+
+    @Test
+    void constructorContext() {
+        try (Context context = Context.newBuilder("js").option("engine.WarnInterpreterOnly", "false")
+                .build()) {
+            GraalJSRegularExpressionFactory factory = new GraalJSRegularExpressionFactory(context);
+            RegularExpression regex = factory.getRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+            assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+        }
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionTest.java
new file mode 100644
index 0000000..1c1fd7d
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/GraalJSRegularExpressionTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+/**
+ * Test for GraalJSRegularExpression.
+ */
+class GraalJSRegularExpressionTest {
+    private static GraalJSRegularExpressionContext CONTEXT = new GraalJSRegularExpressionContext(
+            GraalJSContextFactory.getInstance());
+
+    enum InvalidEscapeInput {
+        A("\\a"),
+        HELLOA("hello\\a"),
+        C("\\c"),
+        E("\\e"),
+        G("\\g"),
+        H("\\h"),
+        I("\\i"),
+        J("\\j"),
+        K("\\k"),
+        L("\\l"),
+        M("\\m"),
+        O("\\o"),
+        Q("\\q"),
+        U("\\u"),
+        X("\\x"),
+        X1("\\x1"),
+        XGG("\\xgg"),
+        X1G("\\x1g"),
+        Y("\\y"),
+        Z("\\z"),
+        _1("\\1"),
+        _2("\\2"),
+        _3("\\3"),
+        _4("\\4"),
+        _5("\\5"),
+        _6("\\6"),
+        _7("\\7"),
+        _8("\\8"),
+        _9("\\9");
+
+        String value;
+
+        InvalidEscapeInput(String value) {
+            this.value = value;
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(InvalidEscapeInput.class)
+    void invalidEscape(InvalidEscapeInput input) {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression(input.value, CONTEXT));
+        assertTrue(e.getMessage().startsWith("SyntaxError"));
+    }
+
+    enum ValidEscapeInput {
+        B("\\b"),
+        D("\\d"),
+        CAP_D("\\D"),
+        W("\\w"),
+        CAP_W("\\W"),
+        S("\\s"),
+        CAP_S("\\S"),
+        T("\\t"),
+        U1234("\\u1234"),
+        R("\\r"),
+        N("\\n"),
+        V("\\v"),
+        F("\\f"),
+        X12("\\x12"),
+        X1F("\\x1f"),
+        X1234("\\x1234"),
+        P("\\p{Letter}cole"), // unicode property
+        CAP_P("\\P{Letter}cole"), // unicode property
+        _0("\\0"),
+        CA("\\cA"), // control
+        CB("\\cB"), // control
+        CC("\\cC"), // control
+        CG("\\cG"); // control
+
+        String value;
+
+        ValidEscapeInput(String value) {
+            this.value = value;
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(ValidEscapeInput.class)
+    void validEscape(ValidEscapeInput input) {
+        assertDoesNotThrow(() -> new GraalJSRegularExpression(input.value, CONTEXT));
+    }
+    
+    @Test
+    void invalidPropertyName() {
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\p", CONTEXT));
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\P", CONTEXT));
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\pa", CONTEXT));
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\Pa", CONTEXT));
+    }
+    
+    @Test
+    void digit() {
+        RegularExpression regex = new GraalJSRegularExpression("\\d", CONTEXT);
+        assertTrue(regex.matches("1"));
+        assertFalse(regex.matches("a"));
+    }
+
+    @Test
+    void invalidEscape() {
+        RuntimeException e = assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("\\a", CONTEXT));
+        assertEquals("SyntaxError: Invalid escape", e.getMessage());
+    }
+
+    @Test
+    void namedCapturingGroup() {
+        RegularExpression regex = new GraalJSRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?", CONTEXT);
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void invalidNamedCapturingGroup() {
+        assertThrows(RuntimeException.class, () -> new GraalJSRegularExpression("(?<name>)(?<name>)", CONTEXT));
+    }
+
+    @Test
+    void namedBackreference() {
+        RegularExpression regex = new GraalJSRegularExpression("title=(?<quote>[\"'])(.*?)\\k<quote>", CONTEXT);
+        assertTrue(regex.matches("title=\"Named capturing groups\\' advantages\""));
+    }
+
+    @Test
+    void concurrency() throws Exception {
+        RegularExpression regex = new GraalJSRegularExpression("\\d", CONTEXT);
+        Exception[] instance = new Exception[1];
+        CountDownLatch latch = new CountDownLatch(1);
+        List<Thread> threads = new ArrayList<>();
+        for (int i = 0; i < 50; ++i) {
+            Runnable runner = new Runnable() {
+                public void run() {
+                    try {
+                        latch.await();
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException(e);
+                    }
+                    try {
+                        assertTrue(regex.matches("1"));
+                    } catch (RuntimeException e) {
+                        instance[0] = e;
+                    }
+                }
+            };
+            Thread thread = new Thread(runner, "Thread" + i);
+            thread.start();
+            threads.add(thread);
+        }
+        latch.countDown(); // Release the latch for threads to run concurrently
+        threads.forEach(t -> {
+            try {
+                t.join();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        });
+        if (instance[0] != null) {
+            throw instance[0];
+        }
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JDKRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..76fcc5f
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionFactoryTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JDKRegularExpressionFactoryTest.
+ */
+class JDKRegularExpressionFactoryTest {
+
+    @Test
+    void getRegularExpression() {
+        JDKRegularExpressionFactory factory = new JDKRegularExpressionFactory();
+        RegularExpression regex = factory
+                .getRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void getInstance() {
+        assertSame(JDKRegularExpressionFactory.getInstance(), JDKRegularExpressionFactory.getInstance());
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JDKRegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionTest.java
new file mode 100644
index 0000000..8cca8a7
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/JDKRegularExpressionTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for JDKRegularExpression.
+ */
+class JDKRegularExpressionTest {
+    @Test
+    void namedCapturingGroup() {
+        RegularExpression regex = new JDKRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void invalidNamedCapturingGroup() {
+        assertThrows(RuntimeException.class, () -> new JDKRegularExpression("(?<name>)(?<name>)"));
+    }
+
+    @Test
+    void namedBackreference() {
+        RegularExpression regex = new JDKRegularExpression("title=(?<quote>[\"'])(.*?)\\k<quote>");
+        assertTrue(regex.matches("title=\"Named capturing groups\\' advantages\""));
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JoniRegularExpressionFactoryTest.java b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionFactoryTest.java
new file mode 100644
index 0000000..fb9c19b
--- /dev/null
+++ b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionFactoryTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed 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 com.networknt.schema.regex;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * JoniRegularExpressionFactoryTest.
+ */
+class JoniRegularExpressionFactoryTest {
+
+    @Test
+    void getRegularExpression() {
+        JoniRegularExpressionFactory factory = new JoniRegularExpressionFactory();
+        RegularExpression regex = factory
+                .getRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void getInstance() {
+        assertSame(JoniRegularExpressionFactory.getInstance(), JoniRegularExpressionFactory.getInstance());
+    }
+}
diff --git a/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java
index fc1f899..9a1d0cd 100644
--- a/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java
+++ b/src/test/java/com/networknt/schema/regex/JoniRegularExpressionTest.java
@@ -18,6 +18,7 @@ package com.networknt.schema.regex;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.joni.exception.SyntaxException;
 import org.junit.jupiter.api.Test;
@@ -111,7 +112,7 @@ class JoniRegularExpressionTest {
     void validEscape(ValidEscapeInput input) {
         assertDoesNotThrow(() -> new JoniRegularExpression(input.value));
     }
-    
+
     @Test
     void invalidPropertyName() {
         assertThrows(SyntaxException.class, () -> new JoniRegularExpression("\\p"));
@@ -119,4 +120,37 @@ class JoniRegularExpressionTest {
         assertThrows(SyntaxException.class, () -> new JoniRegularExpression("\\pa"));
         assertThrows(SyntaxException.class, () -> new JoniRegularExpression("\\Pa"));
     }
+
+    /**
+     * Named capturing group: (?<name>...).
+     * 
+     * @see org.joni.constants.SyntaxProperties#OP2_QMARK_LT_NAMED_GROUP.
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group">Named
+     *      capturing group</a>
+     */
+    @Test
+    void namedCapturingGroup() {
+        RegularExpression regex = new JoniRegularExpression("((?<OrgOID>[^,. ]+)\\s*\\.\\s*(?<AOID>[^,. ]+))(?:\\s*,\\s*)?");
+        assertTrue(regex.matches("FFFF.12645,AAAA.6456"));
+    }
+
+    @Test
+    void invalidNamedCapturingGroup() {
+        assertThrows(RuntimeException.class, () -> new JoniRegularExpression("(?<name>)(?<name>)"));
+    }
+
+    /**
+     * Named capturing group: (?<name>...).
+     * 
+     * @see org.joni.constants.SyntaxProperties#OP2_ESC_K_NAMED_BACKREF.
+     * @see <a href=
+     *      "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_backreference">Named
+     *      backreference</a>
+     */
+    @Test
+    void namedBackreference() {
+        RegularExpression regex = new JoniRegularExpression("title=(?<quote>[\"'])(.*?)\\k<quote>");
+        assertTrue(regex.matches("title=\"Named capturing groups\\' advantages\""));
+    }
 }