Added support class to access Java 9 module API
diff --git a/build.gradle.kts b/build.gradle.kts
index e34574c..548df91 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,7 +20,7 @@
 import java.io.FileOutputStream
 import java.nio.charset.StandardCharsets
 import java.nio.file.Files
-import java.util.Properties
+import java.util.*
 import java.util.stream.Collectors
 
 plugins {
@@ -61,7 +61,11 @@
     configureSourceSet("jython20")
     configureSourceSet("jython22")
     configureSourceSet("jython25") { enableTests() }
-    configureSourceSet("core16", "16") { enableTests() }
+    configureSourceSet("core9", "9") { enableTests() }
+    configureSourceSet("core16", "16") {
+        enableTests();
+        addDependencySourceSet("core9");
+    }
 
     configureGeneratedSourceSet("jakartaServlet") {
         val jakartaSourceGenerators = generateJakartaSources("javaxServlet")
@@ -589,6 +593,7 @@
         // so make a best effort for a combined classpath.
         plusConfigurations = listOf(
             configurations["combinedClasspath"],
+            configurations["core9CompileClasspath"],
             configurations["core16CompileClasspath"],
             configurations["testUtilsCompileClasspath"],
             configurations["javaxServletTestCompileClasspath"]
diff --git a/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt b/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt
index ff23f7a..35dd452 100644
--- a/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt
+++ b/buildSrc/src/main/kotlin/freemarker/build/FreemarkerRootExtension.kt
@@ -19,7 +19,6 @@
 
 package freemarker.build
 
-import java.util.concurrent.atomic.AtomicBoolean
 import org.gradle.api.NamedDomainObjectProvider
 import org.gradle.api.Project
 import org.gradle.api.artifacts.VersionCatalogsExtension
@@ -36,14 +35,11 @@
 import org.gradle.api.tasks.javadoc.Javadoc
 import org.gradle.jvm.toolchain.JavaLanguageVersion
 import org.gradle.jvm.toolchain.JavaToolchainService
-import org.gradle.kotlin.dsl.dependencies
-import org.gradle.kotlin.dsl.named
-import org.gradle.kotlin.dsl.register
-import org.gradle.kotlin.dsl.setProperty
-import org.gradle.kotlin.dsl.the
+import org.gradle.kotlin.dsl.*
 import org.gradle.language.base.plugins.LifecycleBasePlugin
 import org.gradle.language.jvm.tasks.ProcessResources
 import org.gradle.testing.base.TestingExtension
+import java.util.concurrent.atomic.AtomicBoolean
 
 private const val TEST_UTILS_SOURCE_SET_NAME = "test-utils"
 
@@ -146,6 +142,11 @@
         }
     }
 
+    fun addDependencySourceSet(dependencySourceSetName: String) {
+        val dependencySourceSet = context.sourceSets.named(dependencySourceSetName).get();
+        context.inheritCompileRuntimeAndOutput(sourceSet, dependencySourceSet)
+    }
+
     fun enableTests(testJavaVersion: String = ext.testJavaVersion) =
         configureTests(JavaLanguageVersion.of(testJavaVersion))
 
diff --git a/freemarker-core/src/main/java/freemarker/core/_Java9.java b/freemarker-core/src/main/java/freemarker/core/_Java9.java
new file mode 100644
index 0000000..6f1a56e
--- /dev/null
+++ b/freemarker-core/src/main/java/freemarker/core/_Java9.java
@@ -0,0 +1,29 @@
+/*
+ * 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 freemarker.core;
+
+/**
+ * Used internally only, might change without notice!
+ * Used for accessing functionality that's only present in Java 9 or later.
+ */
+public interface _Java9 {
+
+    boolean isAccessibleAccordingToModuleExports(Class<?> m);
+
+}
diff --git a/freemarker-core/src/main/java/freemarker/core/_JavaVersions.java b/freemarker-core/src/main/java/freemarker/core/_JavaVersions.java
index c47d936..4a3957e 100644
--- a/freemarker-core/src/main/java/freemarker/core/_JavaVersions.java
+++ b/freemarker-core/src/main/java/freemarker/core/_JavaVersions.java
@@ -32,6 +32,13 @@
     }
 
     /**
+     * {@code null} if Java 9 is not available, otherwise the object through with the Java 9 operations are available.
+     */
+    static public final _Java9 JAVA_9 = isAtLeast(9, "java.lang.Module")
+            ? tryLoadJavaSupportSingleton(9, _Java9.class)
+            : null;
+
+    /**
      * {@code null} if Java 16 is not available, otherwise the object through with the Java 16 operations are available.
      */
     static public final _Java16 JAVA_16 = isAtLeast(16, "java.net.UnixDomainSocketAddress")
diff --git a/freemarker-core9/src/main/java/freemarker/core/_Java9Impl.java b/freemarker-core9/src/main/java/freemarker/core/_Java9Impl.java
new file mode 100644
index 0000000..700d801
--- /dev/null
+++ b/freemarker-core9/src/main/java/freemarker/core/_Java9Impl.java
@@ -0,0 +1,46 @@
+/*
+ * 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 freemarker.core;
+
+import freemarker.ext.beans.BeansWrapper;
+
+/**
+ * Used internally only, might change without notice!
+ * Used for accessing functionality that's only present in Java 9 or later.
+ */
+// Compile this against Java 9
+@SuppressWarnings("Since15") // For IntelliJ inspection
+public class _Java9Impl implements _Java9 {
+
+    public static final _Java9 INSTANCE = new _Java9Impl();
+
+    private static final Module ACCESSOR_MODULE = BeansWrapper.class.getModule();
+
+    private _Java9Impl() {
+        // Not meant to be instantiated
+    }
+
+    @Override
+    public boolean isAccessibleAccordingToModuleExports(Class<?> accessedClass) {
+        Module accessedModule = accessedClass.getModule();
+        Package accessedPackage = accessedClass.getPackage();
+
+        return accessedModule.isExported(accessedPackage.getName(), ACCESSOR_MODULE);
+    }
+}
diff --git a/freemarker-core9/src/test/java/freemarker/core/Java9ImplTest.java b/freemarker-core9/src/test/java/freemarker/core/Java9ImplTest.java
new file mode 100644
index 0000000..35cd1a4
--- /dev/null
+++ b/freemarker-core9/src/test/java/freemarker/core/Java9ImplTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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 freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Method;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class Java9ImplTest {
+
+    @Test
+    public void testIsAccessibleAccordingToModuleExports() throws Exception {
+        assertNotNull(_JavaVersions.JAVA_9);
+        assertTrue(_JavaVersions.JAVA_9.isAccessibleAccordingToModuleExports(Document.class));
+        assertFalse(_JavaVersions.JAVA_9.isAccessibleAccordingToModuleExports(getSomeInternalClass()));
+    }
+
+    private static Class<?> getSomeInternalClass() throws SAXException, IOException, ParserConfigurationException,
+            NoSuchMethodException {
+        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+                .parse(new InputSource(new StringReader("<a></a>")));
+
+        Method internalClassMethod = document.getClass().getMethod("getDocumentElement");
+        Class<?> internalClass = internalClassMethod.getDeclaringClass();
+        assertThat(internalClass.getName(), Matchers.startsWith("com."));
+
+        return internalClass;
+    }
+}