diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index a7b5eeaad60c..01962b867d6b 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -284,6 +284,9 @@ jobs:
if: ${{ matrix.java == '21' }}
run: .github/retry.sh ant $OPTS -f platform/masterfs test
+ - name: platform/o.n.jdk.fallback
+ run: ant $OPTS -f platform/o.n.jdk.fallback test
+
- name: Commit Validation tests
run: .github/retry.sh ant $OPTS -Dcluster.config=$CLUSTER_CONFIG commit-validation
diff --git a/nbbuild/cluster.properties b/nbbuild/cluster.properties
index bbd52bc8c877..a1f133f3891a 100644
--- a/nbbuild/cluster.properties
+++ b/nbbuild/cluster.properties
@@ -215,6 +215,7 @@ nb.cluster.platform=\
o.n.html.ko4j,\
o.n.html.presenters.spi,\
o.n.html.xhr4j,\
+ o.n.jdk.fallback,\
o.n.swing.laf.dark,\
o.n.swing.laf.flatlaf,\
o.n.swing.outline,\
diff --git a/platform/o.n.jdk.fallback/build.xml b/platform/o.n.jdk.fallback/build.xml
new file mode 100644
index 000000000000..9fcdc9bcee3f
--- /dev/null
+++ b/platform/o.n.jdk.fallback/build.xml
@@ -0,0 +1,25 @@
+
+
+
+ Builds, tests, and runs the project org.netbeans.jdk.fallback
+
+
diff --git a/platform/o.n.jdk.fallback/manifest.mf b/platform/o.n.jdk.fallback/manifest.mf
new file mode 100644
index 000000000000..32b7d9726894
--- /dev/null
+++ b/platform/o.n.jdk.fallback/manifest.mf
@@ -0,0 +1,6 @@
+Manifest-Version: 1.0
+AutoUpdate-Show-In-Client: false
+AutoUpdate-Essential-Module: true
+OpenIDE-Module: o.n.jdk.fallback/0
+OpenIDE-Module-Localizing-Bundle: org/netbeans/jdk/fallback/Bundle.properties
+OpenIDE-Module-Specification-Version: 0.1
diff --git a/platform/o.n.jdk.fallback/nbproject/project.properties b/platform/o.n.jdk.fallback/nbproject/project.properties
new file mode 100644
index 000000000000..e75020298da1
--- /dev/null
+++ b/platform/o.n.jdk.fallback/nbproject/project.properties
@@ -0,0 +1,23 @@
+# 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.
+
+javac.compilerargs=-Xlint
+javac.release=17
+
+# javadoc.arch=${basedir}/arch.xml
+# javadoc.apichanges=${basedir}/apichanges.xml
+# javadoc.arch=${basedir}/arch.xml
diff --git a/platform/o.n.jdk.fallback/nbproject/project.xml b/platform/o.n.jdk.fallback/nbproject/project.xml
new file mode 100644
index 000000000000..d4647ac0d2d3
--- /dev/null
+++ b/platform/o.n.jdk.fallback/nbproject/project.xml
@@ -0,0 +1,48 @@
+
+
+
+ org.netbeans.modules.apisupport.project
+
+
+ o.n.jdk.fallback
+
+
+
+ unit
+
+ org.netbeans.libs.junit4
+
+
+
+ org.netbeans.modules.nbjunit
+
+
+
+
+
+
+ org.netbeans.modules.maven.indexer
+ org.netbeans.jdk.fallback.lang
+
+
+
+
diff --git a/platform/o.n.jdk.fallback/src/org/netbeans/jdk/fallback/Bundle.properties b/platform/o.n.jdk.fallback/src/org/netbeans/jdk/fallback/Bundle.properties
new file mode 100644
index 000000000000..fe8a6095209e
--- /dev/null
+++ b/platform/o.n.jdk.fallback/src/org/netbeans/jdk/fallback/Bundle.properties
@@ -0,0 +1,21 @@
+# 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.
+
+OpenIDE-Module-Name=Simple JDK Fallbacks
+OpenIDE-Module-Display-Category=Libraries
+OpenIDE-Module-Short-Description=Provides internal, non-permanent and possibly incomplete fallbacks for JDK APIs\
+ which are useful for NetBeans but not covered by the minimal run requirements yet.
diff --git a/platform/o.n.jdk.fallback/src/org/netbeans/jdk/fallback/lang/NBLazyConstant.java b/platform/o.n.jdk.fallback/src/org/netbeans/jdk/fallback/lang/NBLazyConstant.java
new file mode 100644
index 000000000000..34528cc071eb
--- /dev/null
+++ b/platform/o.n.jdk.fallback/src/org/netbeans/jdk/fallback/lang/NBLazyConstant.java
@@ -0,0 +1,125 @@
+/*
+ * 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.netbeans.jdk.fallback.lang;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Delegates to JDK's LazyConstant and provides a fallback implementation if not available.
+ *
+ * Internal API, may be removed when no longer needed.
+ *
+ * @author mbien
+ */
+public final class NBLazyConstant {
+
+ private static final MethodHandle lazyConstantFactory;
+
+ static {
+ Logger log = Logger.getLogger(NBLazyConstant.class.getName());
+ MethodHandle mh = null;
+ try {
+ if (Boolean.getBoolean("nb.jdk.LazyConstant.usefallback")) {
+ mh = null;
+ log.log(Level.INFO, "using fallback");
+ } else if (Runtime.version().feature() >= 26) {
+ Class> entryPoint = Class.forName("java.lang.LazyConstant");
+ mh = MethodHandles.lookup().findStatic(entryPoint, "of", MethodType.methodType(entryPoint, Supplier.class))
+ .asType(MethodType.methodType(Supplier.class, Supplier.class));
+ } else if (Runtime.version().feature() == 25) {
+ Class> entryPoint = Class.forName("java.lang.StableValue");
+ mh = MethodHandles.lookup().findStatic(entryPoint, "supplier", MethodType.methodType(Supplier.class, Supplier.class));
+ }
+ // dryrun - just to be sure
+ if (mh != null) {
+ Supplier> probe = () -> true;
+ ((Supplier>)mh.invokeExact(probe)).get();
+ }
+ } catch (Throwable ex) {
+ mh = null;
+ log.log(Level.FINE, "using fallback", ex);
+ }
+ lazyConstantFactory = mh;
+ log.log(Level.FINE, () -> "impl=" + String.valueOf(lazyConstantFactory));
+ }
+
+ private NBLazyConstant() {}
+
+ /**
+ * Create a {@link Supplier} for a lazily initializing constant.
+ * @param computingFunction Factory to create the constant, only called once.
+ * @return Returns the constant, never null.
+ */
+ @SuppressWarnings("unchecked")
+ public static Supplier of(Supplier extends T> computingFunction) {
+ Objects.requireNonNull(computingFunction);
+ if (computingFunction instanceof DoubleCheckedFallback extends T> lc) {
+ return (Supplier) lc;
+ }
+ if (lazyConstantFactory != null) {
+ try {
+ return (Supplier) lazyConstantFactory.invokeExact(computingFunction);
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable ex) {
+ throw new RuntimeException(ex); // shouldn't be reachable under regular circumstances
+ }
+ } else {
+ return new DoubleCheckedFallback<>(computingFunction);
+ }
+ }
+
+ private static class DoubleCheckedFallback implements Supplier {
+
+ private volatile T constant;
+ private Supplier extends T> factory;
+
+ private DoubleCheckedFallback(Supplier extends T> factory) {
+ this.factory = factory;
+ }
+
+ @Override
+ public T get() {
+ T c = constant;
+ if (c == null) {
+ synchronized (this) {
+ c = constant;
+ if (c == null) {
+ c = factory.get();
+ Objects.requireNonNull(c);
+ constant = c;
+ factory = null;
+ }
+ }
+ }
+ return c;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{factory=" + factory + ", constant=" + constant + '}';
+ }
+ }
+}
diff --git a/platform/o.n.jdk.fallback/test/unit/src/org/netbeans/jdk/fallback/lang/NBLazyConstantTest.java b/platform/o.n.jdk.fallback/test/unit/src/org/netbeans/jdk/fallback/lang/NBLazyConstantTest.java
new file mode 100644
index 000000000000..19efa289a302
--- /dev/null
+++ b/platform/o.n.jdk.fallback/test/unit/src/org/netbeans/jdk/fallback/lang/NBLazyConstantTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.netbeans.jdk.fallback.lang;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
+
+public class NBLazyConstantTest {
+
+ @Test
+ public void testFactory() {
+
+ Supplier lazy = NBLazyConstant.of(() -> true);
+ assertNotNull(lazy);
+ assertTrue(lazy.get());
+
+ String impl;
+ if (Runtime.version().feature() >= 26) {
+ impl = "LazyConstant";
+ } else if (Runtime.version().feature() == 25) {
+ impl = "StableSupplier";
+ } else {
+ impl = "DoubleCheckedFallback";
+ }
+ assertTrue(impl + " expected but got " + lazy.getClass(), lazy.getClass().getSimpleName().contains(impl));
+ }
+
+ @Test
+ public void testConstant() {
+ Supplier lazy = NBLazyConstant.of(() -> Math.random());
+ assertNotNull(lazy);
+ assertEquals(lazy.get(), lazy.get());
+ assertEquals(lazy.get(), lazy.get());
+ assertEquals(lazy.get(), lazy.get());
+ }
+
+ @Test
+ public void testRequireNPEOnNullResult() {
+
+ // StableValue allows null, we use the LazyConstant spec
+ assumeTrue(Runtime.version().feature() != 25);
+
+ AtomicReference